mirror of
https://github.com/penpot/penpot.git
synced 2026-01-31 09:41:53 -05:00
Compare commits
15 Commits
test-inner
...
elenatorro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7524594fa | ||
|
|
a940c08da9 | ||
|
|
3de4473251 | ||
|
|
0735140f07 | ||
|
|
dc8a07099d | ||
|
|
90dcf04fb0 | ||
|
|
63959a22cc | ||
|
|
8840246425 | ||
|
|
62ec66cd15 | ||
|
|
e3b87390f6 | ||
|
|
d9ab28e6ed | ||
|
|
9183dbbc43 | ||
|
|
74d00473e9 | ||
|
|
1c70f5a36b | ||
|
|
b23e0c0642 |
28
.github/workflows/tests.yml
vendored
28
.github/workflows/tests.yml
vendored
@@ -8,8 +8,6 @@ on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
- synchronize
|
||||
push:
|
||||
branches:
|
||||
@@ -17,7 +15,7 @@ on:
|
||||
- staging
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
group: ${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
@@ -91,6 +89,30 @@ jobs:
|
||||
run: |
|
||||
yarn run lint:scss;
|
||||
|
||||
test-render-wasm:
|
||||
name: "Render WASM Tests"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Format
|
||||
working-directory: ./render-wasm
|
||||
run: |
|
||||
cargo fmt --check
|
||||
|
||||
- name: Lint
|
||||
working-directory: ./render-wasm
|
||||
run: |
|
||||
./lint
|
||||
|
||||
- name: Test
|
||||
working-directory: ./render-wasm
|
||||
run: |
|
||||
./test
|
||||
|
||||
test-backend:
|
||||
name: "Backend Tests"
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
@@ -318,3 +318,35 @@
|
||||
;; check that we have all no objects
|
||||
(let [rows (th/db-exec! ["select * from storage_object where deleted_at is null"])]
|
||||
(t/is (= 0 (count rows))))))
|
||||
|
||||
(t/deftest tempfile-bucket-test
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
content1 (sto/content "content1")
|
||||
now (ct/now)
|
||||
|
||||
object1 (sto/put-object! storage {::sto/content content1
|
||||
::sto/touched-at (ct/plus now {:minutes 1})
|
||||
:bucket "tempfile"
|
||||
:content-type "text/plain"})]
|
||||
|
||||
|
||||
(binding [ct/*clock* (clock/fixed now)]
|
||||
(let [res (th/run-task! :storage-gc-touched {})]
|
||||
(t/is (= 0 (:freeze res)))
|
||||
(t/is (= 0 (:delete res)))))
|
||||
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/plus now {:minutes 1}))]
|
||||
(let [res (th/run-task! :storage-gc-touched {})]
|
||||
(t/is (= 0 (:freeze res)))
|
||||
(t/is (= 1 (:delete res)))))
|
||||
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/plus now {:hours 1}))]
|
||||
(let [res (th/run-task! :storage-gc-deleted {})]
|
||||
(t/is (= 0 (:deleted res)))))
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/plus now {:hours 2}))]
|
||||
(let [res (th/run-task! :storage-gc-deleted {})]
|
||||
(t/is (= 0 (:deleted res)))))))
|
||||
|
||||
@@ -122,12 +122,16 @@
|
||||
(defn get-u32
|
||||
"A cached variant of get-unsigned-parts"
|
||||
[this]
|
||||
(let [buffer (unchecked-get this "__u32_buffer")]
|
||||
(if (nil? buffer)
|
||||
(let [buffer (get-unsigned-parts this)]
|
||||
(unchecked-set this "__u32_buffer" buffer)
|
||||
buffer)
|
||||
buffer))))
|
||||
(if (some? this)
|
||||
(let [buffer (unchecked-get this "__u32_buffer")]
|
||||
(if (nil? buffer)
|
||||
(let [buffer (get-unsigned-parts this)]
|
||||
(unchecked-set this "__u32_buffer" buffer)
|
||||
buffer)
|
||||
buffer))
|
||||
(do
|
||||
(js/console.warn "get-u32 called with null UUID")
|
||||
nil))))
|
||||
|
||||
#?(:clj
|
||||
(defn hash-int
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"build:storybook:cljs": "clojure -M:dev:shadow-cljs compile storybook",
|
||||
"build:app:libs": "node ./scripts/build-libs.js",
|
||||
"build:app:main": "clojure -M:dev:shadow-cljs release main worker",
|
||||
"build:app:worker": "clojure -M:dev:shadow-cljs release worker",
|
||||
"build:app": "yarn run clear:shadow-cache && yarn run build:app:main && yarn run build:app:libs",
|
||||
"e2e:server": "node ./scripts/e2e-server.js",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
@@ -44,9 +45,9 @@
|
||||
"translations": "node ./scripts/translations.js",
|
||||
"watch:app:assets": "node ./scripts/watch.js",
|
||||
"watch:app:libs": "node ./scripts/build-libs.js --watch",
|
||||
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
|
||||
"watch:app:main": "clojure -M:dev:shadow-cljs watch main storybook",
|
||||
"clear:shadow-cache": "rm -rf .shadow-cljs",
|
||||
"watch:app": "yarn run clear:shadow-cache && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
|
||||
"watch:app": "yarn run clear:shadow-cache && concurrently \"yarn run build:app:worker\" \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
|
||||
"watch": "yarn run watch:app:assets",
|
||||
"watch:storybook": "yarn run build:storybook:assets && concurrently \"storybook dev -p 6006 --no-open\" \"yarn run watch:storybook:assets\"",
|
||||
"watch:storybook:assets": "node ./scripts/watch-storybook.js"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,7 +42,7 @@ export class WasmWorkspacePage extends WorkspacePage {
|
||||
}
|
||||
|
||||
async waitForFirstRenderWithoutUI() {
|
||||
await waitForFirstRender();
|
||||
await this.waitForFirstRender();
|
||||
await this.hideUI();
|
||||
}
|
||||
|
||||
|
||||
@@ -258,6 +258,22 @@ test("Renders a file with nested frames with inherited blur", async ({
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with nested clipping frames", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile(
|
||||
"render-wasm/get-file-frame-nested-clipping.json",
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "44471494-966a-8178-8006-c5bd93f0fe72",
|
||||
pageId: "44471494-966a-8178-8006-c5bd93f0fe73",
|
||||
});
|
||||
await workspace.waitForFirstRenderWithoutUI();
|
||||
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a clipped frame with a large blur drop shadow", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -317,7 +317,7 @@
|
||||
max-height (max height selrect-height)
|
||||
valign (-> shape :content :vertical-align)
|
||||
y (:y selrect)
|
||||
y (if (> height selrect-height)
|
||||
y (if (and valign (> height selrect-height))
|
||||
(case valign
|
||||
"bottom" (- y (- height selrect-height))
|
||||
"center" (- y (/ (- height selrect-height) 2))
|
||||
|
||||
@@ -38,30 +38,18 @@
|
||||
(features/use-feature "render-wasm/v1")
|
||||
|
||||
has-invalid-shapes?
|
||||
(if render-wasm-enabled?
|
||||
false
|
||||
(some (fn [shape]
|
||||
(or (cfh/frame-shape? shape)
|
||||
(cfh/text-shape? shape)))
|
||||
shapes-with-children))
|
||||
(some (if render-wasm-enabled?
|
||||
cfh/frame-shape?
|
||||
#(or (cfh/frame-shape? %) (cfh/text-shape? %)))
|
||||
shapes-with-children)
|
||||
|
||||
head-not-group-like?
|
||||
(and (= 1 total-selected)
|
||||
(not is-group?)
|
||||
(not is-bool?))
|
||||
|
||||
disabled-bool-btns
|
||||
(if render-wasm-enabled?
|
||||
false
|
||||
(or (zero? total-selected)
|
||||
has-invalid-shapes?
|
||||
head-not-group-like?))
|
||||
|
||||
disabled-flatten
|
||||
(if render-wasm-enabled?
|
||||
false
|
||||
(or (zero? total-selected)
|
||||
has-invalid-shapes?))
|
||||
disabled-bool-btns (or (zero? total-selected) has-invalid-shapes? head-not-group-like?)
|
||||
disabled-flatten (or (zero? total-selected) has-invalid-shapes?)
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
|
||||
@@ -216,18 +216,6 @@
|
||||
on-frame-leave (actions/on-frame-leave frame-hover)
|
||||
on-frame-select (actions/on-frame-select selected read-only?)
|
||||
|
||||
;; Text Editor Event Handlers
|
||||
on-text-keydown (fn [event]
|
||||
(when (and text-editing? (.-key event))
|
||||
(.preventDefault event)
|
||||
(wasm.api/handle-text-keydown (.-key event))))
|
||||
on-text-mousedown (fn [event]
|
||||
(when text-editing?
|
||||
(let [rect (.getBoundingClientRect (.-currentTarget event))
|
||||
x (- (.-clientX event) (.-left rect))
|
||||
y (- (.-clientY event) (.-top rect))]
|
||||
(wasm.api/handle-text-mousedown x y))))
|
||||
|
||||
disable-events? (contains? layout :comments)
|
||||
show-comments? (= drawing-tool :comments)
|
||||
show-cursor-tooltip? tooltip
|
||||
@@ -243,9 +231,8 @@
|
||||
|
||||
show-pixel-grid? (and (contains? layout :show-pixel-grid)
|
||||
(>= zoom 8))
|
||||
;; show-text-editor? (and editing-shape (= :text (:type editing-shape)))
|
||||
show-text-editor? (and editing-shape (= :text (:type editing-shape)))
|
||||
|
||||
show-text-editor? false
|
||||
hover-grid? (and (some? @hover-top-frame-id)
|
||||
(ctl/grid-layout? objects @hover-top-frame-id))
|
||||
|
||||
@@ -432,9 +419,7 @@
|
||||
:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-pointer-move on-pointer-move
|
||||
:on-pointer-up on-pointer-up
|
||||
:on-key-down on-text-keydown
|
||||
:on-mouse-down on-text-mousedown}
|
||||
:on-pointer-up on-pointer-up}
|
||||
|
||||
[:defs
|
||||
;; This clip is so the handlers are not over the rulers
|
||||
|
||||
@@ -475,9 +475,9 @@
|
||||
(dissoc :style)
|
||||
(merge style)
|
||||
(select-keys allowed-keys))
|
||||
fill-rule (or (-> attrs :fill-rule sr/translate-fill-rule) (-> attrs :fillRule sr/translate-fill-rule))
|
||||
stroke-linecap (or (-> attrs :stroke-linecap sr/translate-stroke-linecap) (-> attrs :strokeLinecap sr/translate-stroke-linecap))
|
||||
stroke-linejoin (or (-> attrs :stroke-linejoin sr/translate-stroke-linejoin) (-> attrs :strokeLinejoin sr/translate-stroke-linejoin))
|
||||
fill-rule (-> (or (:fill-rule attrs) (:fillRule attrs)) sr/translate-fill-rule)
|
||||
stroke-linecap (-> (or (:stroke-linecap attrs) (:strokeLinecap attrs)) sr/translate-stroke-linecap)
|
||||
stroke-linejoin (-> (or (:stroke-linejoin attrs) (:strokeLinejoin attrs)) sr/translate-stroke-linejoin)
|
||||
fill-none (= "none" (-> attrs :fill))]
|
||||
(h/call wasm/internal-module "_set_shape_svg_attrs" fill-rule stroke-linecap stroke-linejoin fill-none)))
|
||||
|
||||
@@ -1212,7 +1212,6 @@
|
||||
|
||||
(defn init-canvas-context
|
||||
[canvas]
|
||||
|
||||
(let [gl (unchecked-get wasm/internal-module "GL")
|
||||
flags (debug-flags)
|
||||
context-id (if (dbg/enabled? :wasm-gl-context-init-error) "fail" "webgl2")
|
||||
@@ -1259,19 +1258,6 @@
|
||||
(h/call wasm/internal-module "_hide_grid")
|
||||
(request-render "clear-grid"))
|
||||
|
||||
;; Text Editor Functions
|
||||
(defn handle-text-keydown
|
||||
[key]
|
||||
(when (and wasm/internal-module key)
|
||||
(h/call wasm/internal-module "_handle_keydown" key)
|
||||
(request-render "text-editor-keydown")))
|
||||
|
||||
(defn handle-text-mousedown
|
||||
[x y]
|
||||
(when (and wasm/internal-module x y)
|
||||
(h/call wasm/internal-module "_handle_mousedown" x y)
|
||||
(request-render "text-editor-mousedown")))
|
||||
|
||||
(defn get-grid-coords
|
||||
[position]
|
||||
(let [offset (h/call wasm/internal-module
|
||||
|
||||
9
render-wasm/Cargo.lock
generated
9
render-wasm/Cargo.lock
generated
@@ -432,7 +432,6 @@ dependencies = [
|
||||
"indexmap",
|
||||
"macros",
|
||||
"skia-safe",
|
||||
"text_editor",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -578,14 +577,6 @@ dependencies = [
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "text_editor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"skia-safe",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.19"
|
||||
|
||||
@@ -32,7 +32,6 @@ skia-safe = { version = "0.87.0", default-features = false, features = [
|
||||
"binary-cache",
|
||||
"webp",
|
||||
] }
|
||||
text_editor = { path = "src/text_editor" }
|
||||
uuid = { version = "1.11.0", features = ["v4", "js"] }
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -650,38 +650,6 @@ pub extern "C" fn set_modifiers() {
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn handle_keydown(key_ptr: *const std::os::raw::c_char) {
|
||||
if key_ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let key = unsafe {
|
||||
match std::ffi::CStr::from_ptr(key_ptr).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return, // Invalid UTF-8, skip
|
||||
}
|
||||
};
|
||||
|
||||
with_state_mut!(state, {
|
||||
state.render_state.text_editor.handle_keydown(key);
|
||||
});
|
||||
render_sync();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn handle_mousedown(x: f32, y: f32) {
|
||||
// Basic sanity checks
|
||||
if !x.is_finite() || !y.is_finite() {
|
||||
return;
|
||||
}
|
||||
|
||||
with_state_mut!(state, {
|
||||
state.render_state.text_editor.handle_mousedown(x, y);
|
||||
});
|
||||
render_sync();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
init_gl!();
|
||||
|
||||
@@ -38,12 +38,14 @@ const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 1;
|
||||
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
||||
const NODE_BATCH_THRESHOLD: i32 = 10;
|
||||
|
||||
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
||||
|
||||
pub struct NodeRenderState {
|
||||
pub id: Uuid,
|
||||
// We use this bool to keep that we've traversed all the children inside this node.
|
||||
visited_children: bool,
|
||||
// This is used to clip the content of frames.
|
||||
clip_bounds: Option<(Rect, Option<Corners>, Matrix)>,
|
||||
clip_bounds: Option<ClipStack>,
|
||||
// This is a flag to indicate that we've already drawn the mask of a masked group.
|
||||
visited_mask: bool,
|
||||
// This bool indicates that we're drawing the mask shape.
|
||||
@@ -68,13 +70,26 @@ impl NodeRenderState {
|
||||
/// the clipping region to compensate for coordinate system transformations.
|
||||
/// This is useful for nested coordinate systems or when elements are grouped
|
||||
/// and need relative positioning adjustments.
|
||||
fn append_clip(
|
||||
clip_stack: Option<ClipStack>,
|
||||
clip: (Rect, Option<Corners>, Matrix),
|
||||
) -> Option<ClipStack> {
|
||||
match clip_stack {
|
||||
Some(mut stack) => {
|
||||
stack.push(clip);
|
||||
Some(stack)
|
||||
}
|
||||
None => Some(vec![clip]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_children_clip_bounds(
|
||||
&self,
|
||||
element: &Shape,
|
||||
offset: Option<(f32, f32)>,
|
||||
) -> Option<(Rect, Option<Corners>, Matrix)> {
|
||||
) -> Option<ClipStack> {
|
||||
if self.id.is_nil() || !element.clip() {
|
||||
return self.clip_bounds;
|
||||
return self.clip_bounds.clone();
|
||||
}
|
||||
|
||||
let mut bounds = element.selrect();
|
||||
@@ -95,7 +110,7 @@ impl NodeRenderState {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Some((bounds, corners, transform))
|
||||
Self::append_clip(self.clip_bounds.clone(), (bounds, corners, transform))
|
||||
}
|
||||
|
||||
/// Calculates the clip bounds for shadow rendering of a given shape.
|
||||
@@ -113,9 +128,9 @@ impl NodeRenderState {
|
||||
&self,
|
||||
element: &Shape,
|
||||
shadow: &Shadow,
|
||||
) -> Option<(Rect, Option<Corners>, Matrix)> {
|
||||
) -> Option<ClipStack> {
|
||||
if self.id.is_nil() {
|
||||
return self.clip_bounds;
|
||||
return self.clip_bounds.clone();
|
||||
}
|
||||
|
||||
// Assert that the shape is either a Frame or Group
|
||||
@@ -136,9 +151,9 @@ impl NodeRenderState {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Some((bounds, corners, transform))
|
||||
Self::append_clip(self.clip_bounds.clone(), (bounds, corners, transform))
|
||||
}
|
||||
_ => self.clip_bounds,
|
||||
_ => self.clip_bounds.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,7 +230,6 @@ pub(crate) struct RenderState {
|
||||
pub options: RenderOptions,
|
||||
pub surfaces: Surfaces,
|
||||
pub fonts: FontStore,
|
||||
pub text_editor: ::text_editor::TextEditor,
|
||||
pub viewbox: Viewbox,
|
||||
pub cached_viewbox: Viewbox,
|
||||
pub cached_target_snapshot: Option<skia::Image>,
|
||||
@@ -289,14 +303,12 @@ impl RenderState {
|
||||
|
||||
let viewbox = Viewbox::new(width as f32, height as f32);
|
||||
let tiles = tiles::TileHashMap::new();
|
||||
let text_editor = ::text_editor::TextEditor::new(fonts.debug_font.clone());
|
||||
|
||||
RenderState {
|
||||
gpu_state: gpu_state.clone(),
|
||||
options: RenderOptions::default(),
|
||||
surfaces,
|
||||
fonts,
|
||||
text_editor,
|
||||
viewbox,
|
||||
cached_viewbox: Viewbox::new(0., 0.),
|
||||
cached_target_snapshot: None,
|
||||
@@ -371,6 +383,15 @@ impl RenderState {
|
||||
Self::blur_from_variance(total)
|
||||
}
|
||||
|
||||
fn frame_clip_layer_blur(shape: &Shape) -> Option<Blur> {
|
||||
match shape.shape_type {
|
||||
Type::Frame(_) if shape.clip() => shape.blur.filter(|blur| {
|
||||
!blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `f` with `ignore_nested_blurs` temporarily forced to `true`.
|
||||
/// Certain off-screen passes (e.g. shadow masks) must render shapes without
|
||||
/// inheriting ancestor blur. This helper guarantees the flag is restored.
|
||||
@@ -557,7 +578,7 @@ impl RenderState {
|
||||
pub fn render_shape(
|
||||
&mut self,
|
||||
shape: &Shape,
|
||||
clip_bounds: Option<(Rect, Option<Corners>, Matrix)>,
|
||||
clip_bounds: Option<ClipStack>,
|
||||
fills_surface_id: SurfaceId,
|
||||
strokes_surface_id: SurfaceId,
|
||||
innershadows_surface_id: SurfaceId,
|
||||
@@ -577,49 +598,59 @@ impl RenderState {
|
||||
let antialias = shape.should_use_antialias(self.get_scale());
|
||||
|
||||
// set clipping
|
||||
if let Some((bounds, corners, transform)) = clip_bounds {
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().concat(&transform);
|
||||
});
|
||||
if let Some(clips) = clip_bounds.as_ref() {
|
||||
for (bounds, corners, transform) in clips.iter() {
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().concat(transform);
|
||||
});
|
||||
|
||||
if let Some(corners) = corners {
|
||||
let rrect = RRect::new_rect_radii(*bounds, corners);
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas()
|
||||
.clip_rrect(rrect, skia::ClipOp::Intersect, antialias);
|
||||
});
|
||||
} else {
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas()
|
||||
.clip_rect(*bounds, skia::ClipOp::Intersect, antialias);
|
||||
});
|
||||
}
|
||||
|
||||
// This renders a red line around clipped
|
||||
// shapes (frames).
|
||||
if self.options.is_debug_visible() {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
paint.set_color(skia::Color::from_argb(255, 255, 0, 0));
|
||||
paint.set_stroke_width(4.);
|
||||
self.surfaces
|
||||
.canvas(fills_surface_id)
|
||||
.draw_rect(*bounds, &paint);
|
||||
}
|
||||
|
||||
if let Some(corners) = corners {
|
||||
let rrect = RRect::new_rect_radii(bounds, &corners);
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas()
|
||||
.clip_rrect(rrect, skia::ClipOp::Intersect, antialias);
|
||||
});
|
||||
} else {
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas()
|
||||
.clip_rect(bounds, skia::ClipOp::Intersect, antialias);
|
||||
.concat(&transform.invert().unwrap_or(Matrix::default()));
|
||||
});
|
||||
}
|
||||
|
||||
// This renders a red line around clipped
|
||||
// shapes (frames).
|
||||
if self.options.is_debug_visible() {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
paint.set_color(skia::Color::from_argb(255, 255, 0, 0));
|
||||
paint.set_stroke_width(4.);
|
||||
self.surfaces
|
||||
.canvas(fills_surface_id)
|
||||
.draw_rect(bounds, &paint);
|
||||
}
|
||||
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas()
|
||||
.concat(&transform.invert().unwrap_or(Matrix::default()));
|
||||
});
|
||||
}
|
||||
|
||||
// We don't want to change the value in the global state
|
||||
let mut shape: Cow<Shape> = Cow::Borrowed(shape);
|
||||
let frame_has_blur = Self::frame_clip_layer_blur(&shape).is_some();
|
||||
let shape_has_blur = shape.blur.is_some();
|
||||
|
||||
if !self.ignore_nested_blurs {
|
||||
if self.ignore_nested_blurs {
|
||||
if frame_has_blur && shape_has_blur {
|
||||
shape.to_mut().set_blur(None);
|
||||
}
|
||||
} else if !frame_has_blur {
|
||||
if let Some(blur) = self.combined_layer_blur(shape.blur) {
|
||||
shape.to_mut().set_blur(Some(blur));
|
||||
}
|
||||
} else if shape_has_blur {
|
||||
shape.to_mut().set_blur(None);
|
||||
}
|
||||
|
||||
let center = shape.center();
|
||||
@@ -919,8 +950,6 @@ impl RenderState {
|
||||
ui::render(self, shapes);
|
||||
debug::render_wasm_label(self);
|
||||
|
||||
self.text_editor.render(self.surfaces.canvas(SurfaceId::Target));
|
||||
|
||||
self.flush_and_submit();
|
||||
}
|
||||
}
|
||||
@@ -1069,6 +1098,14 @@ impl RenderState {
|
||||
paint.set_blend_mode(element.blend_mode().into());
|
||||
paint.set_alpha_f(element.opacity());
|
||||
|
||||
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
|
||||
let scale = self.get_scale();
|
||||
let sigma = frame_blur.value * scale;
|
||||
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
// When we're rendering the mask shape we need to set a special blend mode
|
||||
// called 'destination-in' that keeps the drawn content within the mask.
|
||||
// @see https://skia.org/docs/user/api/skblendmode_overview/
|
||||
@@ -1233,7 +1270,7 @@ impl RenderState {
|
||||
shape: &Shape,
|
||||
shape_bounds: &Rect,
|
||||
shadow: &Shadow,
|
||||
clip_bounds: Option<(Rect, Option<Corners>, Matrix)>,
|
||||
clip_bounds: Option<ClipStack>,
|
||||
scale: f32,
|
||||
translation: (f32, f32),
|
||||
extra_layer_blur: Option<Blur>,
|
||||
@@ -1378,13 +1415,11 @@ impl RenderState {
|
||||
let mut is_empty = true;
|
||||
|
||||
while let Some(node_render_state) = self.pending_nodes.pop() {
|
||||
let NodeRenderState {
|
||||
id: node_id,
|
||||
visited_children,
|
||||
clip_bounds,
|
||||
visited_mask,
|
||||
mask,
|
||||
} = node_render_state;
|
||||
let node_id = node_render_state.id;
|
||||
let visited_children = node_render_state.visited_children;
|
||||
let visited_mask = node_render_state.visited_mask;
|
||||
let mask = node_render_state.mask;
|
||||
let clip_bounds = node_render_state.clip_bounds.clone();
|
||||
|
||||
is_empty = false;
|
||||
|
||||
@@ -1467,7 +1502,7 @@ impl RenderState {
|
||||
element,
|
||||
&element.extrect(tree, scale),
|
||||
shadow,
|
||||
clip_bounds,
|
||||
clip_bounds.clone(),
|
||||
scale,
|
||||
translation,
|
||||
None,
|
||||
@@ -1555,37 +1590,40 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((bounds, corners, transform)) = clip_bounds.as_ref() {
|
||||
if let Some(clips) = clip_bounds.as_ref() {
|
||||
let antialias = element.should_use_antialias(scale);
|
||||
let mut total_matrix = Matrix::new_identity();
|
||||
total_matrix.pre_scale((scale, scale), None);
|
||||
total_matrix.pre_translate((translation.0, translation.1));
|
||||
total_matrix.pre_concat(transform);
|
||||
|
||||
self.surfaces.canvas(SurfaceId::Current).save();
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.concat(&total_matrix);
|
||||
for (bounds, corners, transform) in clips.iter() {
|
||||
let mut total_matrix = Matrix::new_identity();
|
||||
total_matrix.pre_scale((scale, scale), None);
|
||||
total_matrix.pre_translate((translation.0, translation.1));
|
||||
total_matrix.pre_concat(transform);
|
||||
|
||||
if let Some(corners) = corners {
|
||||
let rrect = RRect::new_rect_radii(*bounds, corners);
|
||||
self.surfaces.canvas(SurfaceId::Current).clip_rrect(
|
||||
rrect,
|
||||
skia::ClipOp::Intersect,
|
||||
antialias,
|
||||
);
|
||||
} else {
|
||||
self.surfaces.canvas(SurfaceId::Current).clip_rect(
|
||||
*bounds,
|
||||
skia::ClipOp::Intersect,
|
||||
antialias,
|
||||
);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.concat(&total_matrix);
|
||||
|
||||
if let Some(corners) = corners {
|
||||
let rrect = RRect::new_rect_radii(*bounds, corners);
|
||||
self.surfaces.canvas(SurfaceId::Current).clip_rrect(
|
||||
rrect,
|
||||
skia::ClipOp::Intersect,
|
||||
antialias,
|
||||
);
|
||||
} else {
|
||||
self.surfaces.canvas(SurfaceId::Current).clip_rect(
|
||||
*bounds,
|
||||
skia::ClipOp::Intersect,
|
||||
antialias,
|
||||
);
|
||||
}
|
||||
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.concat(&total_matrix.invert().unwrap_or_default());
|
||||
}
|
||||
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.concat(&total_matrix.invert().unwrap_or_default());
|
||||
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, None);
|
||||
|
||||
@@ -1601,7 +1639,7 @@ impl RenderState {
|
||||
|
||||
self.render_shape(
|
||||
element,
|
||||
clip_bounds,
|
||||
clip_bounds.clone(),
|
||||
SurfaceId::Fills,
|
||||
SurfaceId::Strokes,
|
||||
SurfaceId::InnerShadows,
|
||||
@@ -1619,6 +1657,9 @@ impl RenderState {
|
||||
}
|
||||
|
||||
match element.shape_type {
|
||||
Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => {
|
||||
self.nested_blurs.push(None);
|
||||
}
|
||||
Type::Frame(_) | Type::Group(_) => {
|
||||
self.nested_blurs.push(element.blur);
|
||||
}
|
||||
@@ -1629,7 +1670,7 @@ impl RenderState {
|
||||
self.pending_nodes.push(NodeRenderState {
|
||||
id: node_id,
|
||||
visited_children: true,
|
||||
clip_bounds,
|
||||
clip_bounds: clip_bounds.clone(),
|
||||
visited_mask: false,
|
||||
mask,
|
||||
});
|
||||
@@ -1656,7 +1697,7 @@ impl RenderState {
|
||||
self.pending_nodes.push(NodeRenderState {
|
||||
id: **child_id,
|
||||
visited_children: false,
|
||||
clip_bounds: children_clip_bounds,
|
||||
clip_bounds: children_clip_bounds.clone(),
|
||||
visited_mask: false,
|
||||
mask: false,
|
||||
});
|
||||
@@ -1796,8 +1837,6 @@ impl RenderState {
|
||||
ui::render(self, tree);
|
||||
debug::render_wasm_label(self);
|
||||
|
||||
self.text_editor.render(self.surfaces.canvas(SurfaceId::Target));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ pub struct FontStore {
|
||||
font_mgr: FontMgr,
|
||||
font_provider: textlayout::TypefaceFontProvider,
|
||||
font_collection: textlayout::FontCollection,
|
||||
pub debug_font: Font,
|
||||
debug_font: Font,
|
||||
fallback_fonts: HashSet<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -885,19 +885,50 @@ impl Shape {
|
||||
scale: f32,
|
||||
) -> Bounds {
|
||||
let mut rect = bounds.to_rect();
|
||||
let include_children = match self.shape_type {
|
||||
Type::Group(_) => true,
|
||||
Type::Frame(_) => !self.clip_content,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if include_children {
|
||||
for child_id in self.children_ids_iter(false) {
|
||||
if let Some(child_shape) = shapes_pool.get(child_id) {
|
||||
let child_extrect = child_shape.calculate_extrect(shapes_pool, scale);
|
||||
rect.join(child_extrect);
|
||||
match self.shape_type {
|
||||
Type::Group(Group { masked: true }) => {
|
||||
let mut mask_rect: Option<math::Rect> = None;
|
||||
let mut content_rect: Option<math::Rect> = None;
|
||||
|
||||
for (index, child_id) in self.children.iter().enumerate() {
|
||||
if let Some(child_shape) = shapes_pool.get(child_id) {
|
||||
let child_extrect = child_shape.calculate_extrect(shapes_pool, scale);
|
||||
|
||||
if index == 0 {
|
||||
mask_rect = Some(child_extrect);
|
||||
} else {
|
||||
match content_rect.as_mut() {
|
||||
Some(r) => r.join(child_extrect),
|
||||
None => content_rect = Some(child_extrect),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (mask_rect, content_rect) {
|
||||
(Some(mut mask), Some(content)) => {
|
||||
if mask.intersect(content) {
|
||||
rect.join(mask);
|
||||
}
|
||||
}
|
||||
(Some(mask), None) | (None, Some(mask)) => {
|
||||
rect.join(mask);
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
}
|
||||
|
||||
Type::Group(_) | Type::Frame(_) if !self.clip_content => {
|
||||
for child_id in self.children_ids_iter(false) {
|
||||
if let Some(child_shape) = shapes_pool.get(child_id) {
|
||||
let child_extrect = child_shape.calculate_extrect(shapes_pool, scale);
|
||||
rect.join(child_extrect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Bounds::from_rect(&rect)
|
||||
@@ -1426,6 +1457,7 @@ impl Shape {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::ShapesPool;
|
||||
|
||||
fn any_shape() -> Shape {
|
||||
Shape::new(Uuid::nil())
|
||||
@@ -1485,4 +1517,42 @@ mod tests {
|
||||
assert_eq!(shape.selrect().width(), 20.0);
|
||||
assert_eq!(shape.selrect().height(), 20.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masked_group_extrect_matches_mask_intersection() {
|
||||
let mut pool = ShapesPool::new();
|
||||
pool.initialize(3);
|
||||
|
||||
let group_id = Uuid::new_v4();
|
||||
let mask_id = Uuid::new_v4();
|
||||
let content_id = Uuid::new_v4();
|
||||
|
||||
{
|
||||
let group = pool.add_shape(group_id);
|
||||
group.set_shape_type(Type::Group(Group { masked: true }));
|
||||
group.children = vec![mask_id, content_id];
|
||||
}
|
||||
|
||||
{
|
||||
let mask = pool.add_shape(mask_id);
|
||||
mask.set_shape_type(Type::Rect(Rect::default()));
|
||||
mask.set_selrect(0.0, 0.0, 50.0, 50.0);
|
||||
mask.set_parent(group_id);
|
||||
}
|
||||
|
||||
{
|
||||
let content = pool.add_shape(content_id);
|
||||
content.set_shape_type(Type::Rect(Rect::default()));
|
||||
content.set_selrect(-10.0, -10.0, 110.0, 110.0);
|
||||
content.set_parent(group_id);
|
||||
}
|
||||
|
||||
let group = pool.get(&group_id).expect("group should exist");
|
||||
let extrect = group.calculate_extrect(&pool, 1.0);
|
||||
|
||||
assert_eq!(extrect.left, 0.0);
|
||||
assert_eq!(extrect.top, 0.0);
|
||||
assert_eq!(extrect.right, 50.0);
|
||||
assert_eq!(extrect.bottom, 50.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use skia_safe::{self as skia, Path, Point, textlayout::FontCollection};
|
||||
use skia_safe::{self as skia, textlayout::FontCollection, Path, Point};
|
||||
use std::collections::HashMap;
|
||||
|
||||
mod shapes_pool;
|
||||
mod text_editor;
|
||||
pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef};
|
||||
pub use text_editor::*;
|
||||
|
||||
use crate::render::RenderState;
|
||||
use crate::shapes::Shape;
|
||||
@@ -18,6 +20,7 @@ use crate::shapes::modifiers::grid_layout::grid_cell_data;
|
||||
/// must not be shared between different Web Workers.
|
||||
pub(crate) struct State<'a> {
|
||||
pub render_state: RenderState,
|
||||
pub text_editor_state: TextEditorState,
|
||||
pub current_id: Option<Uuid>,
|
||||
pub current_browser: u8,
|
||||
pub shapes: ShapesPool<'a>,
|
||||
@@ -25,9 +28,9 @@ pub(crate) struct State<'a> {
|
||||
|
||||
impl<'a> State<'a> {
|
||||
pub fn new(width: i32, height: i32) -> Self {
|
||||
let render_state = RenderState::new(width, height);
|
||||
State {
|
||||
render_state,
|
||||
render_state: RenderState::new(width, height),
|
||||
text_editor_state: TextEditorState::new(),
|
||||
current_id: None,
|
||||
current_browser: 0,
|
||||
shapes: ShapesPool::new(),
|
||||
@@ -46,6 +49,16 @@ impl<'a> State<'a> {
|
||||
&self.render_state
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn text_editor_state_mut(&mut self) -> &mut TextEditorState {
|
||||
&mut self.text_editor_state
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn text_editor_state(&self) -> &TextEditorState {
|
||||
&self.text_editor_state
|
||||
}
|
||||
|
||||
pub fn render_from_cache(&mut self) {
|
||||
self.render_state.render_from_cache(&self.shapes);
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[package]
|
||||
name = "text_editor"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
skia-safe = { version = "0.87.0", default-features = false, features = ["gl"] }
|
||||
wasm-bindgen = "0.2"
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn handle_keydown(key: String) {
|
||||
// TODO: Handle keydown event
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn handle_mousedown(x: f32, y: f32) {
|
||||
// TODO: Handle mousedown event
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
pub mod events;
|
||||
|
||||
use skia_safe::{Canvas, Font, Paint, Point, Color};
|
||||
|
||||
pub struct TextEditor {
|
||||
text: Vec<String>,
|
||||
font: Font,
|
||||
cursor_pos: Point,
|
||||
}
|
||||
|
||||
impl TextEditor {
|
||||
pub fn new(font: Font) -> Self {
|
||||
TextEditor {
|
||||
text: vec!["Hello, Skia!".to_string()],
|
||||
font,
|
||||
cursor_pos: Point::new(0.0, 0.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, canvas: &Canvas) {
|
||||
let mut paint = Paint::default();
|
||||
paint.set_color(Color::BLACK);
|
||||
paint.set_anti_alias(true);
|
||||
|
||||
for (i, line) in self.text.iter().enumerate() {
|
||||
canvas.draw_str(line, (20.0, 20.0 + (i as f32 * 18.0)), &self.font, &paint);
|
||||
}
|
||||
|
||||
// Draw cursor - with bounds checking
|
||||
let mut cursor_paint = Paint::default();
|
||||
cursor_paint.set_color(Color::BLACK);
|
||||
cursor_paint.set_anti_alias(true);
|
||||
|
||||
let y_idx = self.cursor_pos.y as usize;
|
||||
let x_idx = self.cursor_pos.x as usize;
|
||||
|
||||
if y_idx < self.text.len() {
|
||||
let line = &self.text[y_idx];
|
||||
let safe_x_idx = x_idx.min(line.len());
|
||||
|
||||
let (x, _) = if safe_x_idx > 0 {
|
||||
self.font.measure_str(&line[..safe_x_idx], None)
|
||||
} else {
|
||||
(0.0, skia_safe::Rect::new_empty())
|
||||
};
|
||||
|
||||
let cursor_rect = skia_safe::Rect::from_xywh(
|
||||
20.0 + x,
|
||||
20.0 + (y_idx as f32 * 18.0) - 18.0,
|
||||
1.0,
|
||||
18.0
|
||||
);
|
||||
canvas.draw_rect(cursor_rect, &cursor_paint);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_keydown(&mut self, key: &str) {
|
||||
if self.text.is_empty() {
|
||||
self.text.push(String::new());
|
||||
}
|
||||
|
||||
let y = self.cursor_pos.y as usize;
|
||||
let x = self.cursor_pos.x as usize;
|
||||
|
||||
if y >= self.text.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
match key {
|
||||
"ArrowLeft" => {
|
||||
if x > 0 {
|
||||
self.cursor_pos.x -= 1.0;
|
||||
} else if y > 0 {
|
||||
self.cursor_pos.y -= 1.0;
|
||||
self.cursor_pos.x = self.text[y - 1].len() as f32;
|
||||
}
|
||||
}
|
||||
"ArrowRight" => {
|
||||
if x < self.text[y].len() {
|
||||
self.cursor_pos.x += 1.0;
|
||||
} else if y < self.text.len() - 1 {
|
||||
self.cursor_pos.y += 1.0;
|
||||
self.cursor_pos.x = 0.0;
|
||||
}
|
||||
}
|
||||
"ArrowUp" => {
|
||||
if y > 0 {
|
||||
self.cursor_pos.y -= 1.0;
|
||||
let new_y = y - 1;
|
||||
self.cursor_pos.x = self.cursor_pos.x.min(self.text[new_y].len() as f32);
|
||||
}
|
||||
}
|
||||
"ArrowDown" => {
|
||||
if y < self.text.len() - 1 {
|
||||
self.cursor_pos.y += 1.0;
|
||||
let new_y = y + 1;
|
||||
self.cursor_pos.x = self.cursor_pos.x.min(self.text[new_y].len() as f32);
|
||||
}
|
||||
}
|
||||
"Backspace" => {
|
||||
if x > 0 {
|
||||
self.text[y].remove(x - 1);
|
||||
self.cursor_pos.x -= 1.0;
|
||||
} else if y > 0 {
|
||||
let line = self.text.remove(y);
|
||||
self.cursor_pos.y -= 1.0;
|
||||
self.cursor_pos.x = self.text[y - 1].len() as f32;
|
||||
self.text[y - 1].push_str(&line);
|
||||
}
|
||||
}
|
||||
"Enter" => {
|
||||
let line = self.text[y].split_off(x);
|
||||
self.text.insert(y + 1, line);
|
||||
self.cursor_pos.y += 1.0;
|
||||
self.cursor_pos.x = 0.0;
|
||||
}
|
||||
_ => {
|
||||
if key.len() == 1 {
|
||||
if let Some(ch) = key.chars().next() {
|
||||
self.text[y].insert(x, ch);
|
||||
self.cursor_pos.x += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_mousedown(&mut self, x: f32, y: f32) {
|
||||
println!("@@@ Mouse down at: ({}, {})", x, y);
|
||||
if self.text.is_empty() {
|
||||
self.text.push(String::new());
|
||||
}
|
||||
|
||||
let line_height = 18.0;
|
||||
let y_pos = ((y - 20.0) / line_height).floor().max(0.0) as usize;
|
||||
let y_pos = y_pos.min(self.text.len() - 1);
|
||||
self.cursor_pos.y = y_pos as f32;
|
||||
|
||||
let line = &self.text[y_pos];
|
||||
let mut closest_pos = 0;
|
||||
let mut min_dist = f32::MAX;
|
||||
|
||||
for i in 0..=line.len() {
|
||||
let (width, _) = self.font.measure_str(&line[..i], None);
|
||||
let dist = (x - (20.0 + width)).abs();
|
||||
if dist < min_dist {
|
||||
min_dist = dist;
|
||||
closest_pos = i;
|
||||
}
|
||||
}
|
||||
self.cursor_pos.x = closest_pos as f32;
|
||||
}
|
||||
}
|
||||
@@ -292,7 +292,7 @@ pub extern "C" fn set_shape_text_content() {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
let raw_text_data = RawParagraph::try_from(&bytes).unwrap();
|
||||
|
||||
if let Err(_) = shape.add_paragraph(raw_text_data.into()) {
|
||||
if shape.add_paragraph(raw_text_data.into()).is_err() {
|
||||
println!("Error with set_shape_text_content on {:?}", shape.id);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user