Compare commits

..

1 Commits

Author SHA1 Message Date
alonso.torres
06bf5fa274 WIP 2026-02-24 17:41:54 +01:00
15 changed files with 580 additions and 260 deletions

View File

@@ -58,7 +58,8 @@
:share-id share-id
:object-id (mapv :id objects)
:route "objects"
:skip-children skip-children}
:skip-children skip-children
:wasm "true"}
uri (-> (cf/get :public-uri)
(assoc :path "/render.html")
(assoc :query (u/map->query-string params)))]

View File

@@ -45,7 +45,9 @@
[app.main.ui.shapes.svg-raw :as svg-raw]
[app.main.ui.shapes.text :as text]
[app.main.ui.shapes.text.fontfaces :as ff]
[app.render-wasm.api :as wasm.api]
[app.util.dom :as dom]
[app.util.globals :as g]
[app.util.http :as http]
[app.util.strings :as ust]
[app.util.thumbnails :as th]
@@ -53,6 +55,7 @@
[beicon.v2.core :as rx]
[clojure.set :as set]
[cuerdas.core :as str]
[promesa.core :as p]
[rumext.v2 :as mf]))
(def ^:const viewbox-decimal-precision 3)
@@ -171,6 +174,8 @@
;; Don't wrap svg elements inside a <g> otherwise some can break
[:> svg-raw-wrapper {:shape shape :frame frame}]))))))
(set! wasm.api/shape-wrapper-factory shape-wrapper-factory)
(defn format-viewbox
"Format a viewbox given a rectangle"
[{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}]
@@ -480,6 +485,48 @@
[:& ff/fontfaces-style {:fonts fonts}]
[:& shape-wrapper {:shape object}]]]]))
(mf/defc object-wasm
{::mf/wrap [mf/memo]}
[{:keys [objects object-id embed skip-children]
:or {embed false}
:as props}]
(let [object (get objects object-id)
object (cond-> object
(:hide-fill-on-export object)
(assoc :fills [])
skip-children
(assoc :shapes []))
{:keys [width height] :as bounds}
(gsb/get-object-bounds objects object {:ignore-margin? false})
vbox (format-viewbox bounds)
zoom 1
canvas-ref (mf/use-ref nil)]
(mf/use-effect
(fn []
(let [canvas (mf/ref-val canvas-ref)]
(->> @wasm.api/module
(p/fmap
(fn [ready?]
(when ready?
(try
(when (wasm.api/init-canvas-context canvas)
(wasm.api/initialize-viewport
objects zoom vbox "transparent"
(fn []
(wasm.api/render-sync-shape object-id)
(dom/set-attribute! canvas "id" (dm/str "screenshot-" object-id)))))
(catch :default e
(js/console.error "Error initializing canvas context:" e)
false)))))))))
[:canvas {:ref canvas-ref
:width width
:height height
:style {:background "red"}}]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SPRITES (DEBUG)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -63,7 +63,7 @@
(mf/defc object-svg
{::mf/wrap-props false}
[{:keys [object-id embed skip-children]}]
[{:keys [object-id embed skip-children wasm]}]
(let [objects (mf/deref ref:objects)]
;; Set the globa CSS to assign the page size, needed for PDF
@@ -77,27 +77,42 @@
(mth/ceil height) "px")}))))
(when objects
[:& (mf/provider ctx/is-render?) {:value true}
[:& render/object-svg
{:objects objects
:object-id object-id
:embed embed
:skip-children skip-children}]])))
(if wasm
[:& render/object-wasm
{:objects objects
:object-id object-id
:embed embed
:skip-children skip-children}]
(mf/defc objects-svg
{::mf/wrap-props false}
[{:keys [object-ids embed skip-children]}]
(when-let [objects (mf/deref ref:objects)]
(for [object-id object-ids]
(let [objects (render/adapt-objects-for-shape objects object-id)]
[:& (mf/provider ctx/is-render?) {:value true}
[:& render/object-svg
{:objects objects
:key (str object-id)
:object-id object-id
:embed embed
:skip-children skip-children}]]))))
(mf/defc objects-svg
{::mf/wrap-props false}
[{:keys [object-ids embed skip-children wasm]}]
(when-let [objects (mf/deref ref:objects)]
(for [object-id object-ids]
(let [objects (render/adapt-objects-for-shape objects object-id)]
(if wasm
[:& render/object-wasm
{:objects objects
:key (str object-id)
:object-id object-id
:embed embed
:skip-children skip-children}]
[:& (mf/provider ctx/is-render?) {:value true}
[:& render/object-svg
{:objects objects
:key (str object-id)
:object-id object-id
:embed embed
:skip-children skip-children}]])))))
(defn- fetch-objects-bundle
[& {:keys [file-id page-id share-id object-id] :as options}]
(ptk/reify ::fetch-objects-bundle
@@ -136,7 +151,7 @@
(defn- render-objects
[params]
(try
(let [{:keys [file-id page-id embed share-id object-id skip-children] :as params}
(let [{:keys [file-id page-id embed share-id object-id skip-children wasm] :as params}
(coerce-render-objects-params params)]
(st/emit! (fetch-objects-bundle :file-id file-id :page-id page-id :share-id share-id :object-id object-id))
(if (uuid? object-id)
@@ -147,7 +162,8 @@
:share-id share-id
:object-id object-id
:embed embed
:skip-children skip-children}])
:skip-children skip-children
:wasm wasm}])
(mf/html
[:& objects-svg
@@ -156,7 +172,8 @@
:share-id share-id
:object-ids (into #{} object-id)
:embed embed
:skip-children skip-children}])))
:skip-children skip-children
:wasm wasm}])))
(catch :default cause
(when-let [explain (-> cause ex-data ::sm/explain)]
(js/console.log "Unexpected error")

View File

@@ -23,7 +23,6 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.refs :as refs]
[app.main.render :as render]
[app.main.store :as st]
[app.main.ui.shapes.text]
[app.main.worker :as mw]
@@ -100,6 +99,9 @@
(def noop-fn
(constantly nil))
;;
(def shape-wrapper-factory nil)
(defn- yield-to-browser
"Returns a promise that resolves after yielding to the browser's event loop.
Uses requestAnimationFrame for smooth visual updates during loading."
@@ -115,7 +117,7 @@
(let [objects (mf/deref refs/workspace-page-objects)
shape-wrapper
(mf/with-memo [shape]
(render/shape-wrapper-factory objects))]
(shape-wrapper-factory objects))]
[:svg {:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
@@ -1003,62 +1005,62 @@
(defn set-object
[shape]
(perf/begin-measure "set-object")
(let [shape (svg-filters/apply-svg-derived shape)
id (dm/get-prop shape :id)
type (dm/get-prop shape :type)
(when shape
(let [shape (svg-filters/apply-svg-derived shape)
id (dm/get-prop shape :id)
type (dm/get-prop shape :type)
masked (get shape :masked-group)
masked (get shape :masked-group)
fills (get shape :fills)
strokes (if (= type :group)
[] (get shape :strokes))
children (get shape :shapes)
content (let [content (get shape :content)]
(if (= type :text)
(ensure-text-content content)
content))
bool-type (get shape :bool-type)
grow-type (get shape :grow-type)
blur (get shape :blur)
svg-attrs (get shape :svg-attrs)
shadows (get shape :shadow)]
fills (get shape :fills)
strokes (if (= type :group)
[] (get shape :strokes))
children (get shape :shapes)
content (let [content (get shape :content)]
(if (= type :text)
(ensure-text-content content)
content))
bool-type (get shape :bool-type)
grow-type (get shape :grow-type)
blur (get shape :blur)
svg-attrs (get shape :svg-attrs)
shadows (get shape :shadow)]
(shapes/set-shape-base-props shape)
(shapes/set-shape-base-props shape)
;; Remaining properties that need separate calls (variable-length or conditional)
(set-shape-children children)
(set-shape-blur blur)
(when (= type :group)
(set-masked (boolean masked)))
(when (= type :bool)
(set-shape-bool-type bool-type))
(when (and (some? content)
(or (= type :path)
(= type :bool)))
(set-shape-path-content content))
(when (some? svg-attrs)
(set-shape-svg-attrs svg-attrs))
(when (and (some? content) (= type :svg-raw))
(set-shape-svg-raw-content (get-static-markup shape)))
(set-shape-shadows shadows)
(when (= type :text)
(set-shape-grow-type grow-type))
;; Remaining properties that need separate calls (variable-length or conditional)
(set-shape-children children)
(set-shape-blur blur)
(when (= type :group)
(set-masked (boolean masked)))
(when (= type :bool)
(set-shape-bool-type bool-type))
(when (and (some? content)
(or (= type :path)
(= type :bool)))
(set-shape-path-content content))
(when (some? svg-attrs)
(set-shape-svg-attrs svg-attrs))
(when (and (some? content) (= type :svg-raw))
(set-shape-svg-raw-content (get-static-markup shape)))
(set-shape-shadows shadows)
(when (= type :text)
(set-shape-grow-type grow-type))
(set-shape-layout shape)
(set-layout-data shape)
(let [pending_thumbnails (into [] (concat
(set-shape-text-content id content)
(set-shape-text-images id content true)
(set-shape-fills id fills true)
(set-shape-strokes id strokes true)))
pending_full (into [] (concat
(set-shape-text-images id content false)
(set-shape-fills id fills false)
(set-shape-strokes id strokes false)))]
(perf/end-measure "set-object")
{:thumbnails pending_thumbnails
:full pending_full})))
(set-shape-layout shape)
(set-layout-data shape)
(let [pending_thumbnails (into [] (concat
(set-shape-text-content id content)
(set-shape-text-images id content true)
(set-shape-fills id fills true)
(set-shape-strokes id strokes true)))
pending_full (into [] (concat
(set-shape-text-images id content false)
(set-shape-fills id fills false)
(set-shape-strokes id strokes false)))]
(perf/end-measure "set-object")
{:thumbnails pending_thumbnails
:full pending_full}))))
(defn update-text-layouts
[shapes]
@@ -1656,6 +1658,33 @@
(let [controls-to-blur (dom/query-all (dom/get-element "viewport-controls") ".blurrable")]
(run! #(dom/set-style! % "filter" "blur(4px)") controls-to-blur)))
(defn render-shape-pixels
[shape-id scale]
(let [buffer (uuid/get-u32 shape-id)
offset
(h/call wasm/internal-module "_render_shape_pixels"
(aget buffer 0)
(aget buffer 1)
(aget buffer 2)
(aget buffer 3)
scale)
offset-32
(mem/->offset-32 offset)
heap (mem/get-heap-u8)
heapu32 (mem/get-heap-u32)
length (aget heapu32 (mem/->offset-32 offset))
width (aget heapu32 (+ (mem/->offset-32 offset) 1))
height (aget heapu32 (+ (mem/->offset-32 offset) 2))
result (dr/read-image-bytes heap (+ offset 12) length)]
(mem/free)
result))
(defn init-wasm-module
[module]

View File

@@ -45,6 +45,10 @@
:center (gpt/point cx cy)
:transform (gmt/matrix a b c d e f)}))
(defn read-image-bytes
[heap offset length]
(.slice ^js heap offset (+ offset length)))
(defn read-position-data-entry
[heapu32 heapf32 offset]
(let [paragraph (aget heapu32 (+ offset 0))

View File

@@ -6,6 +6,8 @@
(ns debug
(:require
[app.render-wasm.wasm :as wasm]
[app.render-wasm.api :as wasm.api]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
@@ -458,7 +460,24 @@
[]
(.log js/console (clj->js @http/network-averages)))
(defn print-last-exception
[]
(some-> errors/last-exception ex/print-throwable))
(defn ^:export export-image
[]
(let [objects (dsh/lookup-page-objects @st/state)
shape-id (->> (get-selected @st/state) first)
bytes (wasm.api/render-shape-pixels shape-id 1.0)
blob (js/Blob. #js [bytes] #js {:type "image/png"})
url (.createObjectURL js/URL blob)
a (.createElement js/document "a")]
(set! (.-href a) url)
(set! (.-download a) "export.png")
(.click a)
(.revokeObjectURL js/URL url)
nil))

View File

@@ -742,6 +742,24 @@ pub extern "C" fn end_temp_objects() {
}
}
#[no_mangle]
pub extern "C" fn render_shape_pixels(a: u32, b: u32, c: u32, d: u32, scale: f32) -> *mut u8 {
let id = uuid_from_u32_quartet(a, b, c, d);
with_state_mut!(state, {
let (data, width, height) = state.render_shape_pixels(&id, scale, performance::get_time())
.expect("Cannot render into texture");
let len = data.len() as u32;
let mut buf = Vec::with_capacity(4 + data.len());
buf.extend_from_slice(&len.to_le_bytes());
buf.extend_from_slice(&width.to_le_bytes());
buf.extend_from_slice(&height.to_le_bytes());
buf.extend_from_slice(&data);
mem::write_bytes(buf)
})
}
fn main() {
#[cfg(target_arch = "wasm32")]
init_gl!();

View File

@@ -18,6 +18,7 @@ use std::borrow::Cow;
use std::collections::HashSet;
use gpu_state::GpuState;
use options::RenderOptions;
pub use surfaces::{SurfaceId, Surfaces};
@@ -43,6 +44,7 @@ const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
#[derive(Debug)]
pub struct NodeRenderState {
pub id: Uuid,
// We use this bool to keep that we've traversed all the children inside this node.
@@ -537,7 +539,7 @@ impl RenderState {
);
}
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) {
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>, target: SurfaceId) {
performance::begin_measure!("apply_drawing_to_render_canvas");
let paint = skia::Paint::default();
@@ -545,12 +547,12 @@ impl RenderState {
// Only draw surfaces that have content (dirty flag optimization)
if self.surfaces.is_dirty(SurfaceId::TextDropShadows) {
self.surfaces
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
.draw_into(SurfaceId::TextDropShadows, target, Some(&paint));
}
if self.surfaces.is_dirty(SurfaceId::Fills) {
self.surfaces
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
.draw_into(SurfaceId::Fills, target, Some(&paint));
}
let mut render_overlay_below_strokes = false;
@@ -560,17 +562,17 @@ impl RenderState {
if render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
self.surfaces
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
.draw_into(SurfaceId::InnerShadows, target, Some(&paint));
}
if self.surfaces.is_dirty(SurfaceId::Strokes) {
self.surfaces
.draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint));
.draw_into(SurfaceId::Strokes, target, Some(&paint));
}
if !render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
self.surfaces
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
.draw_into(SurfaceId::InnerShadows, target, Some(&paint));
}
// Build mask of dirty surfaces that need clearing
@@ -643,6 +645,7 @@ impl RenderState {
offset: Option<(f32, f32)>,
parent_shadows: Option<Vec<skia_safe::Paint>>,
spread: Option<f32>,
target_surface: SurfaceId,
) {
let surface_ids = fills_surface_id as u32
| strokes_surface_id as u32
@@ -686,15 +689,16 @@ impl RenderState {
&& !(shape.fills.is_empty() && has_nested_fills)
&& !shape
.svg_attrs
.as_ref()
.is_some_and(|attrs| attrs.fill_none);
.as_ref().is_some_and(|attrs| attrs.fill_none)
&& target_surface != SurfaceId::Export;
if can_render_directly {
let scale = self.get_scale();
let translation = self
.surfaces
.get_render_context_translation(self.render_area, scale);
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
self.surfaces.apply_mut(target_surface as u32, |s| {
let canvas = s.canvas();
canvas.save();
canvas.scale((scale, scale));
@@ -706,7 +710,7 @@ impl RenderState {
shape,
&shape.fills,
antialias,
SurfaceId::Current,
target_surface,
None,
);
@@ -716,12 +720,12 @@ impl RenderState {
self,
shape,
&visible_strokes,
Some(SurfaceId::Current),
Some(target_surface),
antialias,
spread,
);
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
self.surfaces.apply_mut(target_surface as u32, |s| {
s.canvas().restore();
});
@@ -1134,7 +1138,7 @@ impl RenderState {
}
if apply_to_current_surface {
self.apply_drawing_to_render_canvas(Some(&shape));
self.apply_drawing_to_render_canvas(Some(&shape), target_surface);
}
// Only restore if we saved (optimization for simple shapes)
@@ -1296,7 +1300,7 @@ impl RenderState {
self.current_tile = None;
self.render_in_progress = true;
self.apply_drawing_to_render_canvas(None);
self.apply_drawing_to_render_canvas(None, SurfaceId::Current);
if sync_render {
self.render_shape_tree_sync(base_object, tree, timestamp)?;
@@ -1347,6 +1351,51 @@ impl RenderState {
Ok(())
}
pub fn render_shape_pixels(
&mut self,
id: &Uuid,
tree: ShapesPoolRef,
scale: f32,
timestamp: i32,
) -> Result<(Vec<u8>, i32, i32), String> {
let target_surface = SurfaceId::Export;
self.surfaces
.canvas(target_surface)
.clear(skia::Color::TRANSPARENT);
if tree.len() != 0 {
let shape = tree.get(id).unwrap();
let mut extrect = shape.extrect(tree, scale);
let margins = self.surfaces.margins;
extrect.offset((margins.width as f32 / scale, margins.height as f32 / scale));
self.surfaces.resize_export_surface(scale, extrect);
self.surfaces.update_render_context(extrect, scale);
self.pending_nodes.push(NodeRenderState {
id: *id,
visited_children: false,
clip_bounds: None,
visited_mask: false,
mask: false,
});
self.render_shape_tree_partial_uncached(tree, timestamp, false, true)?;
}
self.surfaces.flush_and_submit(&mut self.gpu_state, target_surface);
let image = self.surfaces.snapshot(target_surface);
let data = image.encode(
&mut self.gpu_state.context,
skia::EncodedImageFormat::PNG,
100
).expect("PNG encode failed");
let skia::ISize { width, height } = image.dimensions();
Ok((data.as_bytes().to_vec(), width, height))
}
#[inline]
pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool {
iteration % NODE_BATCH_THRESHOLD == 0
@@ -1354,7 +1403,7 @@ impl RenderState {
}
#[inline]
pub fn render_shape_enter(&mut self, element: &Shape, mask: bool) {
pub fn render_shape_enter(&mut self, element: &Shape, mask: bool, target_surface: SurfaceId) {
// Masked groups needs two rendering passes, the first one rendering
// the content and the second one rendering the mask so we need to do
// an extra save_layer to keep all the masked group separate from
@@ -1369,7 +1418,7 @@ impl RenderState {
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces
.canvas(SurfaceId::Current)
.canvas(target_surface)
.save_layer(&layer_rec);
}
}
@@ -1386,7 +1435,7 @@ impl RenderState {
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
self.surfaces
.canvas(SurfaceId::Current)
.canvas(target_surface)
.save_layer(&mask_rec);
}
@@ -1415,7 +1464,7 @@ impl RenderState {
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces
.canvas(SurfaceId::Current)
.canvas(target_surface)
.save_layer(&layer_rec);
}
@@ -1428,6 +1477,7 @@ impl RenderState {
element: &Shape,
visited_mask: bool,
clip_bounds: Option<ClipStack>,
target_surface: SurfaceId
) {
if visited_mask {
// Because masked groups needs two rendering passes (first drawing
@@ -1435,7 +1485,7 @@ impl RenderState {
// extra restore.
if let Type::Group(group) = element.shape_type {
if group.masked {
self.surfaces.canvas(SurfaceId::Current).restore();
self.surfaces.canvas(target_surface).restore();
}
}
} else {
@@ -1497,6 +1547,7 @@ impl RenderState {
None,
None,
None,
target_surface,
);
}
@@ -1505,7 +1556,7 @@ impl RenderState {
let needs_layer = element.needs_layer();
if needs_layer {
self.surfaces.canvas(SurfaceId::Current).restore();
self.surfaces.canvas(target_surface).restore();
}
self.focus_mode.exit(&element.id);
@@ -1587,6 +1638,7 @@ impl RenderState {
scale: f32,
translation: (f32, f32),
extra_layer_blur: Option<Blur>,
target_surface: SurfaceId
) {
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
transformed_shadow.to_mut().offset = (0.0, 0.0);
@@ -1670,6 +1722,7 @@ impl RenderState {
Some(shadow.offset),
None,
Some(shadow.spread),
target_surface,
);
});
@@ -1716,6 +1769,7 @@ impl RenderState {
Some(shadow.offset), // Offset is geometric
None,
Some(shadow.spread), // Spread is geometric
target_surface,
);
});
@@ -1757,6 +1811,7 @@ impl RenderState {
Some(shadow.offset), // Offset is geometric
None,
Some(shadow.spread), // Spread is geometric
target_surface,
);
});
@@ -1811,6 +1866,7 @@ impl RenderState {
scale: f32,
translation: (f32, f32),
node_render_state: &NodeRenderState,
target_surface: SurfaceId
) {
let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale));
let inherited_layer_blur = match element.shape_type {
@@ -1833,6 +1889,7 @@ impl RenderState {
scale,
translation,
None,
target_surface,
);
if !matches!(element.shape_type, Type::Bool(_)) {
@@ -1862,6 +1919,7 @@ impl RenderState {
scale,
translation,
inherited_layer_blur,
target_surface,
);
} else {
let paint = skia::Paint::default();
@@ -1898,6 +1956,7 @@ impl RenderState {
None,
Some(vec![new_shadow_paint.clone()]),
None,
target_surface,
);
});
self.surfaces.canvas(SurfaceId::DropShadows).restore();
@@ -1916,7 +1975,7 @@ impl RenderState {
if let Some(clips) = clip_bounds.as_ref() {
let antialias = element.should_use_antialias(scale);
self.surfaces.canvas(SurfaceId::Current).save();
self.surfaces.canvas(target_surface).save();
for (bounds, corners, transform) in clips.iter() {
let mut total_matrix = Matrix::new_identity();
total_matrix.pre_scale((scale, scale), None);
@@ -1924,18 +1983,18 @@ impl RenderState {
total_matrix.pre_concat(transform);
self.surfaces
.canvas(SurfaceId::Current)
.canvas(target_surface)
.concat(&total_matrix);
if let Some(corners) = corners {
let rrect = RRect::new_rect_radii(*bounds, corners);
self.surfaces.canvas(SurfaceId::Current).clip_rrect(
self.surfaces.canvas(target_surface).clip_rrect(
rrect,
skia::ClipOp::Intersect,
antialias,
);
} else {
self.surfaces.canvas(SurfaceId::Current).clip_rect(
self.surfaces.canvas(target_surface).clip_rect(
*bounds,
skia::ClipOp::Intersect,
antialias,
@@ -1943,15 +2002,15 @@ impl RenderState {
}
self.surfaces
.canvas(SurfaceId::Current)
.canvas(target_surface)
.concat(&total_matrix.invert().unwrap_or_default());
}
self.surfaces
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, None);
self.surfaces.canvas(SurfaceId::Current).restore();
.draw_into(SurfaceId::DropShadows, target_surface, None);
self.surfaces.canvas(target_surface).restore();
} else {
self.surfaces
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, None);
.draw_into(SurfaceId::DropShadows, target_surface, None);
}
self.surfaces
.canvas(SurfaceId::DropShadows)
@@ -1963,10 +2022,16 @@ impl RenderState {
tree: ShapesPoolRef,
timestamp: i32,
allow_stop: bool,
export: bool,
) -> Result<(bool, bool), String> {
let mut iteration = 0;
let mut is_empty = true;
let mut target_surface = SurfaceId::Current;
if export {
target_surface = SurfaceId::Export;
}
while let Some(node_render_state) = self.pending_nodes.pop() {
let node_id = node_render_state.id;
let visited_children = node_render_state.visited_children;
@@ -1992,7 +2057,7 @@ impl RenderState {
if visited_children {
// Skip render_shape_exit for flattened containers
if !element.can_flatten() {
self.render_shape_exit(element, visited_mask, clip_bounds);
self.render_shape_exit(element, visited_mask, clip_bounds, target_surface);
}
continue;
}
@@ -2015,11 +2080,14 @@ impl RenderState {
let has_effects = transformed_element.has_effects_that_extend_bounds();
let is_visible = if is_container || has_effects {
let is_visible = export || if is_container || has_effects {
let element_extrect =
extrect.get_or_insert_with(|| transformed_element.extrect(tree, scale));
element_extrect.intersects(self.render_area)
&& !transformed_element.visually_insignificant(scale, tree)
} else if !has_effects {
// Simple shape: selrect check is sufficient, skip expensive extrect
let selrect = transformed_element.selrect();
selrect.intersects(self.render_area)
} else {
let selrect = transformed_element.selrect();
selrect.intersects(self.render_area)
@@ -2058,6 +2126,7 @@ impl RenderState {
let translation = self
.surfaces
.get_render_context_translation(self.render_area, scale);
self.render_element_drop_shadows_and_composite(
element,
tree,
@@ -2066,10 +2135,11 @@ impl RenderState {
scale,
translation,
&node_render_state,
target_surface,
);
}
self.render_shape_enter(element, mask);
self.render_shape_enter(element, mask, target_surface);
}
if !node_render_state.is_root() && self.focus_mode.is_active() {
@@ -2096,6 +2166,7 @@ impl RenderState {
scale,
translation,
&node_render_state,
target_surface
);
}
@@ -2110,13 +2181,14 @@ impl RenderState {
None,
None,
None,
target_surface,
);
self.surfaces
.canvas(SurfaceId::DropShadows)
.clear(skia::Color::TRANSPARENT);
} else if visited_children {
self.apply_drawing_to_render_canvas(Some(element));
self.apply_drawing_to_render_canvas(Some(element), target_surface);
}
// Skip nested state updates for flattened containers
@@ -2246,7 +2318,7 @@ impl RenderState {
let tile_is_visible = self.tile_viewbox.is_visible(&current_tile);
let can_stop = allow_stop && !tile_is_visible;
let (is_empty, early_return) =
self.render_shape_tree_partial_uncached(tree, timestamp, can_stop)?;
self.render_shape_tree_partial_uncached(tree, timestamp, can_stop, false)?;
if early_return {
return Ok(());

View File

@@ -104,4 +104,38 @@ impl GpuState {
)
.unwrap()
}
#[allow(dead_code)]
pub fn create_surface_from_texture(
&mut self,
width: i32,
height: i32,
texture_id: u32
) -> skia::Surface {
let texture_info = TextureInfo {
target: gl::TEXTURE_2D,
id: texture_id,
format: gl::RGBA8,
protected: skia::gpu::Protected::No,
};
let backend_texture = unsafe{
gpu::backend_textures::make_gl(
(width, height),
gpu::Mipmapped::No,
texture_info,
String::from("export_texture"))
};
gpu::surfaces::wrap_backend_texture(
&mut self.context,
&backend_texture,
gpu::SurfaceOrigin::BottomLeft,
None,
skia::ColorType::RGBA8888,
None,
None,
).unwrap()
}
}

View File

@@ -17,17 +17,18 @@ const TILE_SIZE_MULTIPLIER: i32 = 2;
#[repr(u32)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum SurfaceId {
Target = 0b00_0000_0001,
Filter = 0b00_0000_0010,
Cache = 0b00_0000_0100,
Current = 0b00_0000_1000,
Fills = 0b00_0001_0000,
Strokes = 0b00_0010_0000,
DropShadows = 0b00_0100_0000,
InnerShadows = 0b00_1000_0000,
TextDropShadows = 0b01_0000_0000,
UI = 0b10_0000_0000,
Debug = 0b10_0000_0001,
Target = 0b000_0000_0001,
Filter = 0b000_0000_0010,
Cache = 0b000_0000_0100,
Current = 0b000_0000_1000,
Fills = 0b000_0001_0000,
Strokes = 0b000_0010_0000,
DropShadows = 0b000_0100_0000,
InnerShadows = 0b000_1000_0000,
TextDropShadows = 0b001_0000_0000,
Export = 0b010_0000_0000,
UI = 0b100_0000_0000,
Debug = 0b100_0000_0001,
}
pub struct Surfaces {
@@ -52,11 +53,15 @@ pub struct Surfaces {
// for drawing debug info.
debug: skia::Surface,
// for drawing tiles.
export: skia::Surface,
tiles: TileTextureCache,
sampling_options: skia::SamplingOptions,
margins: skia::ISize,
pub margins: skia::ISize,
// Tracks which surfaces have content (dirty flag bitmask)
dirty_surfaces: u32,
extra_tile_dims: skia::ISize,
}
#[allow(dead_code)]
@@ -77,6 +82,7 @@ impl Surfaces {
let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims);
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height);
let current = gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims);
let drop_shadows =
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims);
let inner_shadows =
@@ -87,10 +93,13 @@ impl Surfaces {
gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims);
let shape_strokes =
gpu_state.create_surface_with_isize("shape_strokes".to_string(), extra_tile_dims);
let export =
gpu_state.create_surface_with_isize("export".to_string(), extra_tile_dims);
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height);
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height);
let tiles = TileTextureCache::new();
Surfaces {
target,
@@ -104,10 +113,12 @@ impl Surfaces {
shape_strokes,
ui,
debug,
export,
tiles,
sampling_options,
margins,
dirty_surfaces: 0,
extra_tile_dims
}
}
@@ -259,6 +270,9 @@ impl Surfaces {
if ids & SurfaceId::Debug as u32 != 0 {
f(self.get_mut(SurfaceId::Debug));
}
if ids & SurfaceId::Export as u32 != 0 {
f(self.get_mut(SurfaceId::Export));
}
performance::begin_measure!("apply_mut::flags");
}
@@ -284,6 +298,7 @@ impl Surfaces {
| SurfaceId::InnerShadows as u32
| SurfaceId::TextDropShadows as u32;
// Clear surfaces before updating transformations to remove residual content
self.apply_mut(surface_ids, |s| {
s.canvas().clear(skia::Color::TRANSPARENT);
@@ -305,7 +320,7 @@ impl Surfaces {
}
#[inline]
fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
pub fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
match id {
SurfaceId::Target => &mut self.target,
SurfaceId::Filter => &mut self.filter,
@@ -318,6 +333,7 @@ impl Surfaces {
SurfaceId::Strokes => &mut self.shape_strokes,
SurfaceId::Debug => &mut self.debug,
SurfaceId::UI => &mut self.ui,
SurfaceId::Export => &mut self.export
}
}
@@ -334,6 +350,7 @@ impl Surfaces {
SurfaceId::Strokes => &self.shape_strokes,
SurfaceId::Debug => &self.debug,
SurfaceId::UI => &self.ui,
SurfaceId::Export => &self.export
}
}
@@ -420,12 +437,14 @@ impl Surfaces {
self.canvas(SurfaceId::TextDropShadows).restore_to_count(1);
self.canvas(SurfaceId::Strokes).restore_to_count(1);
self.canvas(SurfaceId::Current).restore_to_count(1);
self.canvas(SurfaceId::Export).restore_to_count(1);
self.apply_mut(
SurfaceId::Fills as u32
| SurfaceId::Strokes as u32
| SurfaceId::Current as u32
| SurfaceId::InnerShadows as u32
| SurfaceId::TextDropShadows as u32,
| SurfaceId::TextDropShadows as u32
| SurfaceId::Export as u32,
|s| {
s.canvas().clear(color).reset_matrix();
},
@@ -548,6 +567,32 @@ impl Surfaces {
pub fn gc(&mut self) {
self.tiles.gc();
}
pub fn resize_export_surface(&mut self, scale: f32, rect: skia::Rect) {
let target_w = (scale * rect.width()).ceil() as i32;
let target_h = (scale * rect.height()).ceil() as i32;
let max_w = i32::max(self.extra_tile_dims.width, target_w);
let max_h = i32::max(self.extra_tile_dims.height, target_h);
if max_w > self.extra_tile_dims.width || max_h > self.extra_tile_dims.height {
self.extra_tile_dims = skia::ISize::new(max_w, max_h);
self.drop_shadows =
self.drop_shadows.new_surface_with_dimensions((max_w, max_h)).unwrap();
self.inner_shadows =
self.inner_shadows.new_surface_with_dimensions((max_w, max_h)).unwrap();
self.text_drop_shadows =
self.text_drop_shadows.new_surface_with_dimensions((max_w, max_h)).unwrap();
self.text_drop_shadows =
self.text_drop_shadows.new_surface_with_dimensions((max_w, max_h)).unwrap();
self.shape_strokes =
self.shape_strokes.new_surface_with_dimensions((max_w, max_h)).unwrap();
self.shape_fills =
self.shape_strokes.new_surface_with_dimensions((max_w, max_h)).unwrap();
}
self.export = self.export.new_surface_with_dimensions((target_w, target_h)).unwrap();
}
}
pub struct TileTextureCache {
@@ -625,5 +670,5 @@ impl TileTextureCache {
for k in self.grid.keys() {
self.removed.insert(*k);
}
}
}
}

View File

@@ -111,7 +111,7 @@ fn calculate_cursor_rect(
let mut y_offset = vertical_align_offset(shape, &layout_paragraphs);
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
if idx == cursor.paragraph {
let char_pos = cursor.offset;
let char_pos = cursor.char_offset;
// For cursor, we get a zero-width range at the position
// We need to handle edge cases:
// - At start of paragraph: use position 0
@@ -209,13 +209,13 @@ fn calculate_selection_rects(
.sum();
let range_start = if para_idx == start.paragraph {
start.offset
start.char_offset
} else {
0
};
let range_end = if para_idx == end.paragraph {
end.offset
end.char_offset
} else {
para_char_count
};

View File

@@ -14,7 +14,6 @@ use skia_safe::{
textlayout::ParagraphBuilder,
textlayout::ParagraphStyle,
textlayout::PositionWithAffinity,
textlayout::Affinity,
Contains,
};
@@ -113,55 +112,34 @@ impl TextContentSize {
}
}
#[derive(Debug, Clone, Copy, Default)]
#[derive(Debug, Copy, Clone)]
pub struct TextPositionWithAffinity {
#[allow(dead_code)]
pub position_with_affinity: PositionWithAffinity,
pub paragraph: usize,
pub offset: usize,
}
impl PartialEq for TextPositionWithAffinity {
fn eq(&self, other: &Self) -> bool {
self.paragraph == other.paragraph
&& self.offset == other.offset
}
pub paragraph: i32,
#[allow(dead_code)]
pub span: i32,
#[allow(dead_code)]
pub span_relative_offset: i32,
pub offset: i32,
}
impl TextPositionWithAffinity {
pub fn new(
position_with_affinity: PositionWithAffinity,
paragraph: usize,
offset: usize,
paragraph: i32,
span: i32,
span_relative_offset: i32,
offset: i32,
) -> Self {
Self {
position_with_affinity,
paragraph,
span,
span_relative_offset,
offset,
}
}
pub fn empty() -> Self {
Self {
position_with_affinity: PositionWithAffinity {
position: 0,
affinity: Affinity::Downstream,
},
paragraph: 0,
offset: 0,
}
}
pub fn new_without_affinity(paragraph: usize, offset: usize) -> Self {
Self {
position_with_affinity: PositionWithAffinity {
position: offset as i32,
affinity: Affinity::Downstream,
},
paragraph,
offset
}
}
}
#[derive(Debug)]
@@ -455,12 +433,10 @@ impl TextContent {
let mut offset_y = 0.0;
let layout_paragraphs = self.layout.paragraphs.iter().flatten();
let mut paragraph_index: usize = 0;
// IMPORTANT! I'm keeping this because I think it should be better to have the span index
// cached the same way we keep the paragraph index.
#[allow(dead_code)]
let mut _span_index: usize = 0;
let mut paragraph_index: i32 = -1;
let mut span_index: i32 = -1;
for layout_paragraph in layout_paragraphs {
paragraph_index += 1;
let start_y = offset_y;
let end_y = offset_y + layout_paragraph.height();
@@ -481,15 +457,16 @@ impl TextContent {
// Computed position keeps the current position in terms
// of number of characters of text. This is used to know
// in which span we are.
let mut computed_position: usize = 0;
let mut span_offset: usize = 0;
let mut computed_position = 0;
let mut span_offset = 0;
// If paragraph has no spans, default to span 0, offset 0
if paragraph.children().is_empty() {
_span_index = 0;
span_index = 0;
span_offset = 0;
} else {
for span in paragraph.children() {
span_index += 1;
let length = span.text.chars().count();
let start_position = computed_position;
let end_position = computed_position + length;
@@ -506,23 +483,23 @@ impl TextContent {
&& end_position >= current_position
{
span_offset =
position_with_affinity.position as usize - start_position;
position_with_affinity.position - start_position as i32;
break;
}
computed_position += length;
_span_index += 1;
}
}
return Some(TextPositionWithAffinity::new(
position_with_affinity,
paragraph_index,
span_index,
span_offset,
position_with_affinity.position,
));
}
}
offset_y += layout_paragraph.height();
paragraph_index += 1;
}
// Handle completely empty text shapes: if there are no paragraphs or all paragraphs
@@ -539,7 +516,9 @@ impl TextContent {
return Some(TextPositionWithAffinity::new(
default_position,
0, // paragraph 0
0, // span 0
0, // offset 0
0,
));
}

View File

@@ -99,6 +99,11 @@ impl State {
Ok(())
}
pub fn render_shape_pixels(&mut self, id: &Uuid, scale: f32, timestamp: i32) -> Result<(Vec<u8>, i32, i32), String> {
self.render_state
.render_shape_pixels(id, &self.shapes, scale, timestamp)
}
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> {
// If zoom changed, we MUST rebuild the tile index before using it.
// Otherwise, the index will have tiles from the old zoom level, causing visible

View File

@@ -7,10 +7,34 @@ use skia_safe::{
Color,
};
/// Cursor position within text content.
/// Uses character offsets for precise positioning.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
pub struct TextCursor {
pub paragraph: usize,
pub char_offset: usize,
}
impl TextCursor {
pub fn new(paragraph: usize, char_offset: usize) -> Self {
Self {
paragraph,
char_offset,
}
}
pub fn zero() -> Self {
Self {
paragraph: 0,
char_offset: 0,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TextSelection {
pub anchor: TextPositionWithAffinity,
pub focus: TextPositionWithAffinity,
pub anchor: TextCursor,
pub focus: TextCursor,
}
impl TextSelection {
@@ -18,10 +42,10 @@ impl TextSelection {
Self::default()
}
pub fn from_position_with_affinity(position: TextPositionWithAffinity) -> Self {
pub fn from_cursor(cursor: TextCursor) -> Self {
Self {
anchor: position,
focus: position,
anchor: cursor,
focus: cursor,
}
}
@@ -33,12 +57,12 @@ impl TextSelection {
!self.is_collapsed()
}
pub fn set_caret(&mut self, cursor: TextPositionWithAffinity) {
pub fn set_caret(&mut self, cursor: TextCursor) {
self.anchor = cursor;
self.focus = cursor;
}
pub fn extend_to(&mut self, cursor: TextPositionWithAffinity) {
pub fn extend_to(&mut self, cursor: TextCursor) {
self.focus = cursor;
}
@@ -50,24 +74,24 @@ impl TextSelection {
self.focus = self.anchor;
}
pub fn start(&self) -> TextPositionWithAffinity {
pub fn start(&self) -> TextCursor {
if self.anchor.paragraph < self.focus.paragraph {
self.anchor
} else if self.anchor.paragraph > self.focus.paragraph {
self.focus
} else if self.anchor.offset <= self.focus.offset {
} else if self.anchor.char_offset <= self.focus.char_offset {
self.anchor
} else {
self.focus
}
}
pub fn end(&self) -> TextPositionWithAffinity {
pub fn end(&self) -> TextCursor {
if self.anchor.paragraph > self.focus.paragraph {
self.anchor
} else if self.anchor.paragraph < self.focus.paragraph {
self.focus
} else if self.anchor.offset >= self.focus.offset {
} else if self.anchor.char_offset >= self.focus.char_offset {
self.anchor
} else {
self.focus
@@ -78,7 +102,7 @@ impl TextSelection {
/// Events that the text editor can emit for frontend synchronization
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum TextEditorEvent {
pub enum EditorEvent {
None = 0,
ContentChanged = 1,
SelectionChanged = 2,
@@ -107,7 +131,7 @@ pub struct TextEditorState {
pub active_shape_id: Option<Uuid>,
pub cursor_visible: bool,
pub last_blink_time: f64,
pending_events: Vec<TextEditorEvent>,
pending_events: Vec<EditorEvent>,
}
impl TextEditorState {
@@ -203,16 +227,18 @@ impl TextEditorState {
true
}
pub fn set_caret_from_position(&mut self, position: &TextPositionWithAffinity) {
self.selection.set_caret(*position);
pub fn set_caret_from_position(&mut self, position: TextPositionWithAffinity) {
let cursor = TextCursor::new(position.paragraph as usize, position.offset as usize);
self.selection.set_caret(cursor);
self.reset_blink();
self.push_event(TextEditorEvent::SelectionChanged);
self.push_event(EditorEvent::SelectionChanged);
}
pub fn extend_selection_from_position(&mut self, position: &TextPositionWithAffinity) {
self.selection.extend_to(*position);
pub fn extend_selection_from_position(&mut self, position: TextPositionWithAffinity) {
let cursor = TextCursor::new(position.paragraph as usize, position.offset as usize);
self.selection.extend_to(cursor);
self.reset_blink();
self.push_event(TextEditorEvent::SelectionChanged);
self.push_event(EditorEvent::SelectionChanged);
}
pub fn update_blink(&mut self, timestamp_ms: f64) {
@@ -238,17 +264,41 @@ impl TextEditorState {
self.last_blink_time = 0.0;
}
pub fn push_event(&mut self, event: TextEditorEvent) {
pub fn push_event(&mut self, event: EditorEvent) {
if self.pending_events.last() != Some(&event) {
self.pending_events.push(event);
}
}
pub fn poll_event(&mut self) -> TextEditorEvent {
self.pending_events.pop().unwrap_or(TextEditorEvent::None)
pub fn poll_event(&mut self) -> EditorEvent {
self.pending_events.pop().unwrap_or(EditorEvent::None)
}
pub fn has_pending_events(&self) -> bool {
!self.pending_events.is_empty()
}
pub fn set_caret_position_from(
&mut self,
text_position_with_affinity: TextPositionWithAffinity,
) {
self.set_caret_from_position(text_position_with_affinity);
}
}
/// TODO: Remove legacy code
#[derive(Debug, PartialEq, Clone, Copy)]
pub struct TextNodePosition {
pub paragraph: i32,
pub span: i32,
}
impl TextNodePosition {
pub fn new(paragraph: i32, span: i32) -> Self {
Self { paragraph, span }
}
pub fn is_invalid(&self) -> bool {
self.paragraph < 0 || self.span < 0
}
}

View File

@@ -1,7 +1,7 @@
use crate::math::{Matrix, Point, Rect};
use crate::mem;
use crate::shapes::{Paragraph, Shape, TextContent, TextPositionWithAffinity, Type, VerticalAlign};
use crate::state::{TextSelection};
use crate::shapes::{Paragraph, Shape, TextContent, Type, VerticalAlign};
use crate::state::{TextCursor, TextSelection};
use crate::utils::uuid_from_u32_quartet;
use crate::utils::uuid_to_u32_quartet;
use crate::{with_state, with_state_mut, STATE};
@@ -203,7 +203,7 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
{
state
.text_editor_state
.extend_selection_from_position(&position);
.extend_selection_from_position(position);
}
state.text_editor_state.stop_pointer_selection();
});
@@ -231,7 +231,7 @@ pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
if let Some(position) =
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
{
state.text_editor_state.set_caret_from_position(&position);
state.text_editor_state.set_caret_from_position(position);
}
});
}
@@ -276,7 +276,7 @@ pub extern "C" fn text_editor_insert_text() {
let cursor = state.text_editor_state.selection.focus;
if let Some(new_offset) = insert_text_at_cursor(text_content, &cursor, &text) {
let new_cursor = TextPositionWithAffinity::new_without_affinity(cursor.paragraph, new_offset);
let new_cursor = TextCursor::new(cursor.paragraph, new_offset);
state.text_editor_state.selection.set_caret(new_cursor);
}
@@ -286,10 +286,10 @@ pub extern "C" fn text_editor_insert_text() {
state.text_editor_state.reset_blink();
state
.text_editor_state
.push_event(crate::state::TextEditorEvent::ContentChanged);
.push_event(crate::state::EditorEvent::ContentChanged);
state
.text_editor_state
.push_event(crate::state::TextEditorEvent::NeedsLayout);
.push_event(crate::state::EditorEvent::NeedsLayout);
state.render_state.mark_touched(shape_id);
});
@@ -336,10 +336,10 @@ pub extern "C" fn text_editor_delete_backward() {
state.text_editor_state.reset_blink();
state
.text_editor_state
.push_event(crate::state::TextEditorEvent::ContentChanged);
.push_event(crate::state::EditorEvent::ContentChanged);
state
.text_editor_state
.push_event(crate::state::TextEditorEvent::NeedsLayout);
.push_event(crate::state::EditorEvent::NeedsLayout);
state.render_state.mark_touched(shape_id);
});
@@ -384,10 +384,10 @@ pub extern "C" fn text_editor_delete_forward() {
state.text_editor_state.reset_blink();
state
.text_editor_state
.push_event(crate::state::TextEditorEvent::ContentChanged);
.push_event(crate::state::EditorEvent::ContentChanged);
state
.text_editor_state
.push_event(crate::state::TextEditorEvent::NeedsLayout);
.push_event(crate::state::EditorEvent::NeedsLayout);
state.render_state.mark_touched(shape_id);
});
@@ -423,7 +423,7 @@ pub extern "C" fn text_editor_insert_paragraph() {
let cursor = state.text_editor_state.selection.focus;
if split_paragraph_at_cursor(text_content, &cursor) {
let new_cursor = TextPositionWithAffinity::new_without_affinity(cursor.paragraph + 1, 0);
let new_cursor = TextCursor::new(cursor.paragraph + 1, 0);
state.text_editor_state.selection.set_caret(new_cursor);
}
@@ -433,10 +433,10 @@ pub extern "C" fn text_editor_insert_paragraph() {
state.text_editor_state.reset_blink();
state
.text_editor_state
.push_event(crate::state::TextEditorEvent::ContentChanged);
.push_event(crate::state::EditorEvent::ContentChanged);
state
.text_editor_state
.push_event(crate::state::TextEditorEvent::NeedsLayout);
.push_event(crate::state::EditorEvent::NeedsLayout);
state.render_state.mark_touched(shape_id);
});
@@ -494,7 +494,7 @@ pub extern "C" fn text_editor_move_cursor(direction: CursorDirection, extend_sel
state.text_editor_state.reset_blink();
state
.text_editor_state
.push_event(crate::state::TextEditorEvent::SelectionChanged);
.push_event(crate::state::EditorEvent::SelectionChanged);
});
}
@@ -711,12 +711,12 @@ pub extern "C" fn text_editor_export_selection() -> *mut u8 {
.map(|span| span.text.chars().count())
.sum();
let range_start = if para_idx == start.paragraph {
start.offset
start.char_offset
} else {
0
};
let range_end = if para_idx == end.paragraph {
end.offset
end.char_offset
} else {
para_char_count
};
@@ -764,9 +764,9 @@ pub extern "C" fn text_editor_get_selection(buffer_ptr: *mut u32) -> u32 {
let sel = &state.text_editor_state.selection;
unsafe {
*buffer_ptr = sel.anchor.paragraph as u32;
*buffer_ptr.add(1) = sel.anchor.offset as u32;
*buffer_ptr.add(1) = sel.anchor.char_offset as u32;
*buffer_ptr.add(2) = sel.focus.paragraph as u32;
*buffer_ptr.add(3) = sel.focus.offset as u32;
*buffer_ptr.add(3) = sel.focus.char_offset as u32;
}
1
})
@@ -776,7 +776,7 @@ pub extern "C" fn text_editor_get_selection(buffer_ptr: *mut u32) -> u32 {
// HELPERS: Cursor & Selection
// ============================================================================
fn get_cursor_rect(text_content: &TextContent, cursor: &TextPositionWithAffinity, shape: &Shape) -> Option<Rect> {
fn get_cursor_rect(text_content: &TextContent, cursor: &TextCursor, shape: &Shape) -> Option<Rect> {
let paragraphs = text_content.paragraphs();
if cursor.paragraph >= paragraphs.len() {
return None;
@@ -794,7 +794,7 @@ fn get_cursor_rect(text_content: &TextContent, cursor: &TextPositionWithAffinity
let mut y_offset = valign_offset;
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
if idx == cursor.paragraph {
let char_pos = cursor.offset;
let char_pos = cursor.char_offset;
use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
let rects = laid_out_para.get_rects_for_range(
@@ -869,13 +869,13 @@ fn get_selection_rects(
.map(|span| span.text.chars().count())
.sum();
let range_start = if para_idx == start.paragraph {
start.offset
start.char_offset
} else {
0
};
let range_end = if para_idx == end.paragraph {
end.offset
end.char_offset
} else {
para_char_count
};
@@ -914,40 +914,40 @@ fn paragraph_char_count(para: &Paragraph) -> usize {
}
/// Clamp a cursor position to valid bounds within the text content.
fn clamp_cursor(position: TextPositionWithAffinity, paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
fn clamp_cursor(cursor: TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
if paragraphs.is_empty() {
return TextPositionWithAffinity::new_without_affinity(0, 0);
return TextCursor::new(0, 0);
}
let para_idx = position.paragraph.min(paragraphs.len() - 1);
let para_idx = cursor.paragraph.min(paragraphs.len() - 1);
let para_len = paragraph_char_count(&paragraphs[para_idx]);
let char_offset = position.offset.min(para_len);
let char_offset = cursor.char_offset.min(para_len);
TextPositionWithAffinity::new_without_affinity(para_idx, char_offset)
TextCursor::new(para_idx, char_offset)
}
/// Move cursor left by one character.
fn move_cursor_backward(cursor: &TextPositionWithAffinity, paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
if cursor.offset > 0 {
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, cursor.offset - 1)
fn move_cursor_backward(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
if cursor.char_offset > 0 {
TextCursor::new(cursor.paragraph, cursor.char_offset - 1)
} else if cursor.paragraph > 0 {
let prev_para = cursor.paragraph - 1;
let char_count = paragraph_char_count(&paragraphs[prev_para]);
TextPositionWithAffinity::new_without_affinity(prev_para, char_count)
TextCursor::new(prev_para, char_count)
} else {
*cursor
}
}
/// Move cursor right by one character.
fn move_cursor_forward(cursor: &TextPositionWithAffinity, paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
fn move_cursor_forward(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
let para = &paragraphs[cursor.paragraph];
let char_count = paragraph_char_count(para);
if cursor.offset < char_count {
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, cursor.offset + 1)
if cursor.char_offset < char_count {
TextCursor::new(cursor.paragraph, cursor.char_offset + 1)
} else if cursor.paragraph < paragraphs.len() - 1 {
TextPositionWithAffinity::new_without_affinity(cursor.paragraph + 1, 0)
TextCursor::new(cursor.paragraph + 1, 0)
} else {
*cursor
}
@@ -955,52 +955,52 @@ fn move_cursor_forward(cursor: &TextPositionWithAffinity, paragraphs: &[Paragrap
/// Move cursor up by one line.
fn move_cursor_up(
cursor: &TextPositionWithAffinity,
cursor: &TextCursor,
paragraphs: &[Paragraph],
_text_content: &TextContent,
_shape: &Shape,
) -> TextPositionWithAffinity {
) -> TextCursor {
// TODO: Implement proper line-based navigation using line metrics
if cursor.paragraph > 0 {
let prev_para = cursor.paragraph - 1;
let char_count = paragraph_char_count(&paragraphs[prev_para]);
let new_offset = cursor.offset.min(char_count);
TextPositionWithAffinity::new_without_affinity(prev_para, new_offset)
let new_offset = cursor.char_offset.min(char_count);
TextCursor::new(prev_para, new_offset)
} else {
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, 0)
TextCursor::new(cursor.paragraph, 0)
}
}
/// Move cursor down by one line.
fn move_cursor_down(
cursor: &TextPositionWithAffinity,
cursor: &TextCursor,
paragraphs: &[Paragraph],
_text_content: &TextContent,
_shape: &Shape,
) -> TextPositionWithAffinity {
) -> TextCursor {
// TODO: Implement proper line-based navigation using line metrics
if cursor.paragraph < paragraphs.len() - 1 {
let next_para = cursor.paragraph + 1;
let char_count = paragraph_char_count(&paragraphs[next_para]);
let new_offset = cursor.offset.min(char_count);
TextPositionWithAffinity::new_without_affinity(next_para, new_offset)
let new_offset = cursor.char_offset.min(char_count);
TextCursor::new(next_para, new_offset)
} else {
let char_count = paragraph_char_count(&paragraphs[cursor.paragraph]);
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, char_count)
TextCursor::new(cursor.paragraph, char_count)
}
}
/// Move cursor to start of current line.
fn move_cursor_line_start(cursor: &TextPositionWithAffinity, _paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
fn move_cursor_line_start(cursor: &TextCursor, _paragraphs: &[Paragraph]) -> TextCursor {
// TODO: Implement proper line-start using line metrics
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, 0)
TextCursor::new(cursor.paragraph, 0)
}
/// Move cursor to end of current line.
fn move_cursor_line_end(cursor: &TextPositionWithAffinity, paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
fn move_cursor_line_end(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
// TODO: Implement proper line-end using line metrics
let char_count = paragraph_char_count(&paragraphs[cursor.paragraph]);
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, char_count)
TextCursor::new(cursor.paragraph, char_count)
}
// ============================================================================
@@ -1028,7 +1028,7 @@ fn find_span_at_offset(para: &Paragraph, char_offset: usize) -> Option<(usize, u
/// Insert text at a cursor position. Returns the new character offset after insertion.
fn insert_text_at_cursor(
text_content: &mut TextContent,
cursor: &TextPositionWithAffinity,
cursor: &TextCursor,
text: &str,
) -> Option<usize> {
let paragraphs = text_content.paragraphs_mut();
@@ -1048,7 +1048,7 @@ fn insert_text_at_cursor(
return Some(text.chars().count());
}
let (span_idx, offset_in_span) = find_span_at_offset(para, cursor.offset)?;
let (span_idx, offset_in_span) = find_span_at_offset(para, cursor.char_offset)?;
let children = para.children_mut();
let span = &mut children[span_idx];
@@ -1063,7 +1063,7 @@ fn insert_text_at_cursor(
new_text.insert_str(byte_offset, text);
span.set_text(new_text);
Some(cursor.offset + text.chars().count())
Some(cursor.char_offset + text.chars().count())
}
/// Delete a range of text specified by a selection.
@@ -1079,18 +1079,18 @@ fn delete_selection_range(text_content: &mut TextContent, selection: &TextSelect
if start.paragraph == end.paragraph {
delete_range_in_paragraph(
&mut paragraphs[start.paragraph],
start.offset,
end.offset,
start.char_offset,
end.char_offset,
);
} else {
let start_para_len = paragraph_char_count(&paragraphs[start.paragraph]);
delete_range_in_paragraph(
&mut paragraphs[start.paragraph],
start.offset,
start.char_offset,
start_para_len,
);
delete_range_in_paragraph(&mut paragraphs[end.paragraph], 0, end.offset);
delete_range_in_paragraph(&mut paragraphs[end.paragraph], 0, end.char_offset);
if end.paragraph < paragraphs.len() {
let end_para_children: Vec<_> =
@@ -1189,13 +1189,13 @@ fn delete_range_in_paragraph(para: &mut Paragraph, start_offset: usize, end_offs
}
/// Delete the character before the cursor. Returns the new cursor position.
fn delete_char_before(text_content: &mut TextContent, cursor: &TextPositionWithAffinity) -> Option<TextPositionWithAffinity> {
if cursor.offset > 0 {
fn delete_char_before(text_content: &mut TextContent, cursor: &TextCursor) -> Option<TextCursor> {
if cursor.char_offset > 0 {
let paragraphs = text_content.paragraphs_mut();
let para = &mut paragraphs[cursor.paragraph];
let delete_pos = cursor.offset - 1;
delete_range_in_paragraph(para, delete_pos, cursor.offset);
Some(TextPositionWithAffinity::new_without_affinity(cursor.paragraph, delete_pos))
let delete_pos = cursor.char_offset - 1;
delete_range_in_paragraph(para, delete_pos, cursor.char_offset);
Some(TextCursor::new(cursor.paragraph, delete_pos))
} else if cursor.paragraph > 0 {
let prev_para_idx = cursor.paragraph - 1;
let paragraphs = text_content.paragraphs_mut();
@@ -1211,14 +1211,14 @@ fn delete_char_before(text_content: &mut TextContent, cursor: &TextPositionWithA
paragraphs.remove(cursor.paragraph);
Some(TextPositionWithAffinity::new_without_affinity(prev_para_idx, prev_para_len))
Some(TextCursor::new(prev_para_idx, prev_para_len))
} else {
None
}
}
/// Delete the character after the cursor.
fn delete_char_after(text_content: &mut TextContent, cursor: &TextPositionWithAffinity) {
fn delete_char_after(text_content: &mut TextContent, cursor: &TextCursor) {
let paragraphs = text_content.paragraphs_mut();
if cursor.paragraph >= paragraphs.len() {
return;
@@ -1226,9 +1226,9 @@ fn delete_char_after(text_content: &mut TextContent, cursor: &TextPositionWithAf
let para_len = paragraph_char_count(&paragraphs[cursor.paragraph]);
if cursor.offset < para_len {
if cursor.char_offset < para_len {
let para = &mut paragraphs[cursor.paragraph];
delete_range_in_paragraph(para, cursor.offset, cursor.offset + 1);
delete_range_in_paragraph(para, cursor.char_offset, cursor.char_offset + 1);
} else if cursor.paragraph < paragraphs.len() - 1 {
let next_para_idx = cursor.paragraph + 1;
let next_children: Vec<_> = paragraphs[next_para_idx].children_mut().drain(..).collect();
@@ -1241,7 +1241,7 @@ fn delete_char_after(text_content: &mut TextContent, cursor: &TextPositionWithAf
}
/// Split a paragraph at the cursor position. Returns true if split was successful.
fn split_paragraph_at_cursor(text_content: &mut TextContent, cursor: &TextPositionWithAffinity) -> bool {
fn split_paragraph_at_cursor(text_content: &mut TextContent, cursor: &TextCursor) -> bool {
let paragraphs = text_content.paragraphs_mut();
if cursor.paragraph >= paragraphs.len() {
return false;
@@ -1249,7 +1249,7 @@ fn split_paragraph_at_cursor(text_content: &mut TextContent, cursor: &TextPositi
let para = &paragraphs[cursor.paragraph];
let Some((span_idx, offset_in_span)) = find_span_at_offset(para, cursor.offset) else {
let Some((span_idx, offset_in_span)) = find_span_at_offset(para, cursor.char_offset) else {
return false;
};