mirror of
https://github.com/penpot/penpot.git
synced 2026-02-24 18:56:28 -05:00
Compare commits
1 Commits
alotor-exp
...
azazeln28-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2e689a645 |
@@ -58,8 +58,7 @@
|
||||
:share-id share-id
|
||||
:object-id (mapv :id objects)
|
||||
:route "objects"
|
||||
:skip-children skip-children
|
||||
:wasm "true"}
|
||||
:skip-children skip-children}
|
||||
uri (-> (cf/get :public-uri)
|
||||
(assoc :path "/render.html")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
|
||||
@@ -45,9 +45,7 @@
|
||||
[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]
|
||||
@@ -55,7 +53,6 @@
|
||||
[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)
|
||||
@@ -174,8 +171,6 @@
|
||||
;; 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}}]
|
||||
@@ -485,48 +480,6 @@
|
||||
[:& 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)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
(mf/defc object-svg
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [object-id embed skip-children wasm]}]
|
||||
[{:keys [object-id embed skip-children]}]
|
||||
(let [objects (mf/deref ref:objects)]
|
||||
|
||||
;; Set the globa CSS to assign the page size, needed for PDF
|
||||
@@ -77,41 +77,26 @@
|
||||
(mth/ceil height) "px")}))))
|
||||
|
||||
(when objects
|
||||
(if wasm
|
||||
[:& render/object-wasm
|
||||
{:objects objects
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}]
|
||||
|
||||
[:& (mf/provider ctx/is-render?) {:value true}
|
||||
[:& render/object-svg
|
||||
{:objects objects
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}]]))))
|
||||
[:& (mf/provider ctx/is-render?) {:value true}
|
||||
[:& render/object-svg
|
||||
{: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 wasm]}]
|
||||
[{: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)]
|
||||
(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}]])))))
|
||||
[:& (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}]
|
||||
@@ -151,7 +136,7 @@
|
||||
(defn- render-objects
|
||||
[params]
|
||||
(try
|
||||
(let [{:keys [file-id page-id embed share-id object-id skip-children wasm] :as params}
|
||||
(let [{:keys [file-id page-id embed share-id object-id skip-children] :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)
|
||||
@@ -162,8 +147,7 @@
|
||||
:share-id share-id
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children
|
||||
:wasm wasm}])
|
||||
:skip-children skip-children}])
|
||||
|
||||
(mf/html
|
||||
[:& objects-svg
|
||||
@@ -172,8 +156,7 @@
|
||||
:share-id share-id
|
||||
:object-ids (into #{} object-id)
|
||||
:embed embed
|
||||
:skip-children skip-children
|
||||
:wasm wasm}])))
|
||||
:skip-children skip-children}])))
|
||||
(catch :default cause
|
||||
(when-let [explain (-> cause ex-data ::sm/explain)]
|
||||
(js/console.log "Unexpected error")
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
[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]
|
||||
@@ -99,9 +100,6 @@
|
||||
(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."
|
||||
@@ -117,7 +115,7 @@
|
||||
(let [objects (mf/deref refs/workspace-page-objects)
|
||||
shape-wrapper
|
||||
(mf/with-memo [shape]
|
||||
(shape-wrapper-factory objects))]
|
||||
(render/shape-wrapper-factory objects))]
|
||||
|
||||
[:svg {:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
@@ -1005,62 +1003,62 @@
|
||||
(defn set-object
|
||||
[shape]
|
||||
(perf/begin-measure "set-object")
|
||||
(when shape
|
||||
(let [shape (svg-filters/apply-svg-derived shape)
|
||||
id (dm/get-prop shape :id)
|
||||
type (dm/get-prop shape :type)
|
||||
(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]
|
||||
@@ -1658,33 +1656,6 @@
|
||||
(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]
|
||||
|
||||
@@ -45,10 +45,6 @@
|
||||
: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))
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
(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]
|
||||
@@ -460,24 +458,7 @@
|
||||
[]
|
||||
(.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))
|
||||
|
||||
@@ -742,24 +742,6 @@ 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!();
|
||||
|
||||
@@ -18,7 +18,6 @@ use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use gpu_state::GpuState;
|
||||
|
||||
use options::RenderOptions;
|
||||
pub use surfaces::{SurfaceId, Surfaces};
|
||||
|
||||
@@ -44,7 +43,6 @@ 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.
|
||||
@@ -539,7 +537,7 @@ impl RenderState {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>, target: SurfaceId) {
|
||||
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) {
|
||||
performance::begin_measure!("apply_drawing_to_render_canvas");
|
||||
|
||||
let paint = skia::Paint::default();
|
||||
@@ -547,12 +545,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, target, Some(&paint));
|
||||
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
|
||||
}
|
||||
|
||||
if self.surfaces.is_dirty(SurfaceId::Fills) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::Fills, target, Some(&paint));
|
||||
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
|
||||
}
|
||||
|
||||
let mut render_overlay_below_strokes = false;
|
||||
@@ -562,17 +560,17 @@ impl RenderState {
|
||||
|
||||
if render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::InnerShadows, target, Some(&paint));
|
||||
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
|
||||
}
|
||||
|
||||
if self.surfaces.is_dirty(SurfaceId::Strokes) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::Strokes, target, Some(&paint));
|
||||
.draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint));
|
||||
}
|
||||
|
||||
if !render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::InnerShadows, target, Some(&paint));
|
||||
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
|
||||
}
|
||||
|
||||
// Build mask of dirty surfaces that need clearing
|
||||
@@ -645,7 +643,6 @@ 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
|
||||
@@ -689,16 +686,15 @@ impl RenderState {
|
||||
&& !(shape.fills.is_empty() && has_nested_fills)
|
||||
&& !shape
|
||||
.svg_attrs
|
||||
.as_ref().is_some_and(|attrs| attrs.fill_none)
|
||||
&& target_surface != SurfaceId::Export;
|
||||
.as_ref()
|
||||
.is_some_and(|attrs| attrs.fill_none);
|
||||
|
||||
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(target_surface as u32, |s| {
|
||||
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
|
||||
let canvas = s.canvas();
|
||||
canvas.save();
|
||||
canvas.scale((scale, scale));
|
||||
@@ -710,7 +706,7 @@ impl RenderState {
|
||||
shape,
|
||||
&shape.fills,
|
||||
antialias,
|
||||
target_surface,
|
||||
SurfaceId::Current,
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -720,12 +716,12 @@ impl RenderState {
|
||||
self,
|
||||
shape,
|
||||
&visible_strokes,
|
||||
Some(target_surface),
|
||||
Some(SurfaceId::Current),
|
||||
antialias,
|
||||
spread,
|
||||
);
|
||||
|
||||
self.surfaces.apply_mut(target_surface as u32, |s| {
|
||||
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
|
||||
s.canvas().restore();
|
||||
});
|
||||
|
||||
@@ -1138,7 +1134,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
if apply_to_current_surface {
|
||||
self.apply_drawing_to_render_canvas(Some(&shape), target_surface);
|
||||
self.apply_drawing_to_render_canvas(Some(&shape));
|
||||
}
|
||||
|
||||
// Only restore if we saved (optimization for simple shapes)
|
||||
@@ -1300,7 +1296,7 @@ impl RenderState {
|
||||
self.current_tile = None;
|
||||
self.render_in_progress = true;
|
||||
|
||||
self.apply_drawing_to_render_canvas(None, SurfaceId::Current);
|
||||
self.apply_drawing_to_render_canvas(None);
|
||||
|
||||
if sync_render {
|
||||
self.render_shape_tree_sync(base_object, tree, timestamp)?;
|
||||
@@ -1351,51 +1347,6 @@ 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
|
||||
@@ -1403,7 +1354,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn render_shape_enter(&mut self, element: &Shape, mask: bool, target_surface: SurfaceId) {
|
||||
pub fn render_shape_enter(&mut self, element: &Shape, mask: bool) {
|
||||
// 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
|
||||
@@ -1418,7 +1369,7 @@ impl RenderState {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
self.surfaces
|
||||
.canvas(target_surface)
|
||||
.canvas(SurfaceId::Current)
|
||||
.save_layer(&layer_rec);
|
||||
}
|
||||
}
|
||||
@@ -1435,7 +1386,7 @@ impl RenderState {
|
||||
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
|
||||
let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
|
||||
self.surfaces
|
||||
.canvas(target_surface)
|
||||
.canvas(SurfaceId::Current)
|
||||
.save_layer(&mask_rec);
|
||||
}
|
||||
|
||||
@@ -1464,7 +1415,7 @@ impl RenderState {
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
self.surfaces
|
||||
.canvas(target_surface)
|
||||
.canvas(SurfaceId::Current)
|
||||
.save_layer(&layer_rec);
|
||||
}
|
||||
|
||||
@@ -1477,7 +1428,6 @@ 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
|
||||
@@ -1485,7 +1435,7 @@ impl RenderState {
|
||||
// extra restore.
|
||||
if let Type::Group(group) = element.shape_type {
|
||||
if group.masked {
|
||||
self.surfaces.canvas(target_surface).restore();
|
||||
self.surfaces.canvas(SurfaceId::Current).restore();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1547,7 +1497,6 @@ impl RenderState {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
target_surface,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1556,7 +1505,7 @@ impl RenderState {
|
||||
let needs_layer = element.needs_layer();
|
||||
|
||||
if needs_layer {
|
||||
self.surfaces.canvas(target_surface).restore();
|
||||
self.surfaces.canvas(SurfaceId::Current).restore();
|
||||
}
|
||||
|
||||
self.focus_mode.exit(&element.id);
|
||||
@@ -1638,7 +1587,6 @@ 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);
|
||||
@@ -1722,7 +1670,6 @@ impl RenderState {
|
||||
Some(shadow.offset),
|
||||
None,
|
||||
Some(shadow.spread),
|
||||
target_surface,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1769,7 +1716,6 @@ impl RenderState {
|
||||
Some(shadow.offset), // Offset is geometric
|
||||
None,
|
||||
Some(shadow.spread), // Spread is geometric
|
||||
target_surface,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1811,7 +1757,6 @@ impl RenderState {
|
||||
Some(shadow.offset), // Offset is geometric
|
||||
None,
|
||||
Some(shadow.spread), // Spread is geometric
|
||||
target_surface,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1866,7 +1811,6 @@ 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 {
|
||||
@@ -1889,7 +1833,6 @@ impl RenderState {
|
||||
scale,
|
||||
translation,
|
||||
None,
|
||||
target_surface,
|
||||
);
|
||||
|
||||
if !matches!(element.shape_type, Type::Bool(_)) {
|
||||
@@ -1919,7 +1862,6 @@ impl RenderState {
|
||||
scale,
|
||||
translation,
|
||||
inherited_layer_blur,
|
||||
target_surface,
|
||||
);
|
||||
} else {
|
||||
let paint = skia::Paint::default();
|
||||
@@ -1956,7 +1898,6 @@ impl RenderState {
|
||||
None,
|
||||
Some(vec![new_shadow_paint.clone()]),
|
||||
None,
|
||||
target_surface,
|
||||
);
|
||||
});
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
@@ -1975,7 +1916,7 @@ impl RenderState {
|
||||
|
||||
if let Some(clips) = clip_bounds.as_ref() {
|
||||
let antialias = element.should_use_antialias(scale);
|
||||
self.surfaces.canvas(target_surface).save();
|
||||
self.surfaces.canvas(SurfaceId::Current).save();
|
||||
for (bounds, corners, transform) in clips.iter() {
|
||||
let mut total_matrix = Matrix::new_identity();
|
||||
total_matrix.pre_scale((scale, scale), None);
|
||||
@@ -1983,18 +1924,18 @@ impl RenderState {
|
||||
total_matrix.pre_concat(transform);
|
||||
|
||||
self.surfaces
|
||||
.canvas(target_surface)
|
||||
.canvas(SurfaceId::Current)
|
||||
.concat(&total_matrix);
|
||||
|
||||
if let Some(corners) = corners {
|
||||
let rrect = RRect::new_rect_radii(*bounds, corners);
|
||||
self.surfaces.canvas(target_surface).clip_rrect(
|
||||
self.surfaces.canvas(SurfaceId::Current).clip_rrect(
|
||||
rrect,
|
||||
skia::ClipOp::Intersect,
|
||||
antialias,
|
||||
);
|
||||
} else {
|
||||
self.surfaces.canvas(target_surface).clip_rect(
|
||||
self.surfaces.canvas(SurfaceId::Current).clip_rect(
|
||||
*bounds,
|
||||
skia::ClipOp::Intersect,
|
||||
antialias,
|
||||
@@ -2002,15 +1943,15 @@ impl RenderState {
|
||||
}
|
||||
|
||||
self.surfaces
|
||||
.canvas(target_surface)
|
||||
.canvas(SurfaceId::Current)
|
||||
.concat(&total_matrix.invert().unwrap_or_default());
|
||||
}
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::DropShadows, target_surface, None);
|
||||
self.surfaces.canvas(target_surface).restore();
|
||||
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, None);
|
||||
self.surfaces.canvas(SurfaceId::Current).restore();
|
||||
} else {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::DropShadows, target_surface, None);
|
||||
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, None);
|
||||
}
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
@@ -2022,16 +1963,10 @@ 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;
|
||||
@@ -2057,7 +1992,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, target_surface);
|
||||
self.render_shape_exit(element, visited_mask, clip_bounds);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -2080,14 +2015,11 @@ impl RenderState {
|
||||
|
||||
let has_effects = transformed_element.has_effects_that_extend_bounds();
|
||||
|
||||
let is_visible = export || if is_container || has_effects {
|
||||
let is_visible = if is_container || has_effects {
|
||||
let element_extrect =
|
||||
extrect.get_or_insert_with(|| transformed_element.extrect(tree, scale));
|
||||
element_extrect.intersects(self.render_area)
|
||||
} else if !has_effects {
|
||||
// Simple shape: selrect check is sufficient, skip expensive extrect
|
||||
let selrect = transformed_element.selrect();
|
||||
selrect.intersects(self.render_area)
|
||||
&& !transformed_element.visually_insignificant(scale, tree)
|
||||
} else {
|
||||
let selrect = transformed_element.selrect();
|
||||
selrect.intersects(self.render_area)
|
||||
@@ -2126,7 +2058,6 @@ impl RenderState {
|
||||
let translation = self
|
||||
.surfaces
|
||||
.get_render_context_translation(self.render_area, scale);
|
||||
|
||||
self.render_element_drop_shadows_and_composite(
|
||||
element,
|
||||
tree,
|
||||
@@ -2135,11 +2066,10 @@ impl RenderState {
|
||||
scale,
|
||||
translation,
|
||||
&node_render_state,
|
||||
target_surface,
|
||||
);
|
||||
}
|
||||
|
||||
self.render_shape_enter(element, mask, target_surface);
|
||||
self.render_shape_enter(element, mask);
|
||||
}
|
||||
|
||||
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
||||
@@ -2166,7 +2096,6 @@ impl RenderState {
|
||||
scale,
|
||||
translation,
|
||||
&node_render_state,
|
||||
target_surface
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2181,14 +2110,13 @@ 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), target_surface);
|
||||
self.apply_drawing_to_render_canvas(Some(element));
|
||||
}
|
||||
|
||||
// Skip nested state updates for flattened containers
|
||||
@@ -2318,7 +2246,7 @@ impl RenderState {
|
||||
let tile_is_visible = self.tile_viewbox.is_visible(¤t_tile);
|
||||
let can_stop = allow_stop && !tile_is_visible;
|
||||
let (is_empty, early_return) =
|
||||
self.render_shape_tree_partial_uncached(tree, timestamp, can_stop, false)?;
|
||||
self.render_shape_tree_partial_uncached(tree, timestamp, can_stop)?;
|
||||
|
||||
if early_return {
|
||||
return Ok(());
|
||||
|
||||
@@ -104,38 +104,4 @@ 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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,18 +17,17 @@ const TILE_SIZE_MULTIPLIER: i32 = 2;
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum SurfaceId {
|
||||
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,
|
||||
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,
|
||||
}
|
||||
|
||||
pub struct Surfaces {
|
||||
@@ -53,15 +52,11 @@ pub struct Surfaces {
|
||||
// for drawing debug info.
|
||||
debug: skia::Surface,
|
||||
// for drawing tiles.
|
||||
export: skia::Surface,
|
||||
|
||||
tiles: TileTextureCache,
|
||||
sampling_options: skia::SamplingOptions,
|
||||
pub margins: skia::ISize,
|
||||
margins: skia::ISize,
|
||||
// Tracks which surfaces have content (dirty flag bitmask)
|
||||
dirty_surfaces: u32,
|
||||
|
||||
extra_tile_dims: skia::ISize,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -82,7 +77,6 @@ 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 =
|
||||
@@ -93,13 +87,10 @@ 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,
|
||||
@@ -113,12 +104,10 @@ impl Surfaces {
|
||||
shape_strokes,
|
||||
ui,
|
||||
debug,
|
||||
export,
|
||||
tiles,
|
||||
sampling_options,
|
||||
margins,
|
||||
dirty_surfaces: 0,
|
||||
extra_tile_dims
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,9 +259,6 @@ 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");
|
||||
}
|
||||
|
||||
@@ -298,7 +284,6 @@ 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);
|
||||
@@ -320,7 +305,7 @@ impl Surfaces {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
|
||||
fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
|
||||
match id {
|
||||
SurfaceId::Target => &mut self.target,
|
||||
SurfaceId::Filter => &mut self.filter,
|
||||
@@ -333,7 +318,6 @@ impl Surfaces {
|
||||
SurfaceId::Strokes => &mut self.shape_strokes,
|
||||
SurfaceId::Debug => &mut self.debug,
|
||||
SurfaceId::UI => &mut self.ui,
|
||||
SurfaceId::Export => &mut self.export
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +334,6 @@ impl Surfaces {
|
||||
SurfaceId::Strokes => &self.shape_strokes,
|
||||
SurfaceId::Debug => &self.debug,
|
||||
SurfaceId::UI => &self.ui,
|
||||
SurfaceId::Export => &self.export
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,14 +420,12 @@ 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::Export as u32,
|
||||
| SurfaceId::TextDropShadows as u32,
|
||||
|s| {
|
||||
s.canvas().clear(color).reset_matrix();
|
||||
},
|
||||
@@ -567,32 +548,6 @@ 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 {
|
||||
@@ -670,5 +625,5 @@ impl TileTextureCache {
|
||||
for k in self.grid.keys() {
|
||||
self.removed.insert(*k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.char_offset;
|
||||
let char_pos = cursor.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.char_offset
|
||||
start.offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let range_end = if para_idx == end.paragraph {
|
||||
end.char_offset
|
||||
end.offset
|
||||
} else {
|
||||
para_char_count
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ use skia_safe::{
|
||||
textlayout::ParagraphBuilder,
|
||||
textlayout::ParagraphStyle,
|
||||
textlayout::PositionWithAffinity,
|
||||
textlayout::Affinity,
|
||||
Contains,
|
||||
};
|
||||
|
||||
@@ -112,34 +113,55 @@ impl TextContentSize {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct TextPositionWithAffinity {
|
||||
#[allow(dead_code)]
|
||||
pub position_with_affinity: PositionWithAffinity,
|
||||
pub paragraph: i32,
|
||||
#[allow(dead_code)]
|
||||
pub span: i32,
|
||||
#[allow(dead_code)]
|
||||
pub span_relative_offset: i32,
|
||||
pub offset: i32,
|
||||
pub paragraph: usize,
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
impl PartialEq for TextPositionWithAffinity {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.paragraph == other.paragraph
|
||||
&& self.offset == other.offset
|
||||
}
|
||||
}
|
||||
|
||||
impl TextPositionWithAffinity {
|
||||
pub fn new(
|
||||
position_with_affinity: PositionWithAffinity,
|
||||
paragraph: i32,
|
||||
span: i32,
|
||||
span_relative_offset: i32,
|
||||
offset: i32,
|
||||
paragraph: usize,
|
||||
offset: usize,
|
||||
) -> 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)]
|
||||
@@ -433,10 +455,12 @@ impl TextContent {
|
||||
let mut offset_y = 0.0;
|
||||
let layout_paragraphs = self.layout.paragraphs.iter().flatten();
|
||||
|
||||
let mut paragraph_index: i32 = -1;
|
||||
let mut span_index: i32 = -1;
|
||||
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;
|
||||
for layout_paragraph in layout_paragraphs {
|
||||
paragraph_index += 1;
|
||||
let start_y = offset_y;
|
||||
let end_y = offset_y + layout_paragraph.height();
|
||||
|
||||
@@ -457,16 +481,15 @@ 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 = 0;
|
||||
let mut span_offset = 0;
|
||||
let mut computed_position: usize = 0;
|
||||
let mut span_offset: usize = 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;
|
||||
@@ -483,23 +506,23 @@ impl TextContent {
|
||||
&& end_position >= current_position
|
||||
{
|
||||
span_offset =
|
||||
position_with_affinity.position - start_position as i32;
|
||||
position_with_affinity.position as usize - start_position;
|
||||
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
|
||||
@@ -516,9 +539,7 @@ impl TextContent {
|
||||
return Some(TextPositionWithAffinity::new(
|
||||
default_position,
|
||||
0, // paragraph 0
|
||||
0, // span 0
|
||||
0, // offset 0
|
||||
0,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -99,11 +99,6 @@ 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
|
||||
|
||||
@@ -7,34 +7,10 @@ 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: TextCursor,
|
||||
pub focus: TextCursor,
|
||||
pub anchor: TextPositionWithAffinity,
|
||||
pub focus: TextPositionWithAffinity,
|
||||
}
|
||||
|
||||
impl TextSelection {
|
||||
@@ -42,10 +18,10 @@ impl TextSelection {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn from_cursor(cursor: TextCursor) -> Self {
|
||||
pub fn from_position_with_affinity(position: TextPositionWithAffinity) -> Self {
|
||||
Self {
|
||||
anchor: cursor,
|
||||
focus: cursor,
|
||||
anchor: position,
|
||||
focus: position,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +33,12 @@ impl TextSelection {
|
||||
!self.is_collapsed()
|
||||
}
|
||||
|
||||
pub fn set_caret(&mut self, cursor: TextCursor) {
|
||||
pub fn set_caret(&mut self, cursor: TextPositionWithAffinity) {
|
||||
self.anchor = cursor;
|
||||
self.focus = cursor;
|
||||
}
|
||||
|
||||
pub fn extend_to(&mut self, cursor: TextCursor) {
|
||||
pub fn extend_to(&mut self, cursor: TextPositionWithAffinity) {
|
||||
self.focus = cursor;
|
||||
}
|
||||
|
||||
@@ -74,24 +50,24 @@ impl TextSelection {
|
||||
self.focus = self.anchor;
|
||||
}
|
||||
|
||||
pub fn start(&self) -> TextCursor {
|
||||
pub fn start(&self) -> TextPositionWithAffinity {
|
||||
if self.anchor.paragraph < self.focus.paragraph {
|
||||
self.anchor
|
||||
} else if self.anchor.paragraph > self.focus.paragraph {
|
||||
self.focus
|
||||
} else if self.anchor.char_offset <= self.focus.char_offset {
|
||||
} else if self.anchor.offset <= self.focus.offset {
|
||||
self.anchor
|
||||
} else {
|
||||
self.focus
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&self) -> TextCursor {
|
||||
pub fn end(&self) -> TextPositionWithAffinity {
|
||||
if self.anchor.paragraph > self.focus.paragraph {
|
||||
self.anchor
|
||||
} else if self.anchor.paragraph < self.focus.paragraph {
|
||||
self.focus
|
||||
} else if self.anchor.char_offset >= self.focus.char_offset {
|
||||
} else if self.anchor.offset >= self.focus.offset {
|
||||
self.anchor
|
||||
} else {
|
||||
self.focus
|
||||
@@ -102,7 +78,7 @@ impl TextSelection {
|
||||
/// Events that the text editor can emit for frontend synchronization
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum EditorEvent {
|
||||
pub enum TextEditorEvent {
|
||||
None = 0,
|
||||
ContentChanged = 1,
|
||||
SelectionChanged = 2,
|
||||
@@ -131,7 +107,7 @@ pub struct TextEditorState {
|
||||
pub active_shape_id: Option<Uuid>,
|
||||
pub cursor_visible: bool,
|
||||
pub last_blink_time: f64,
|
||||
pending_events: Vec<EditorEvent>,
|
||||
pending_events: Vec<TextEditorEvent>,
|
||||
}
|
||||
|
||||
impl TextEditorState {
|
||||
@@ -227,18 +203,16 @@ impl TextEditorState {
|
||||
true
|
||||
}
|
||||
|
||||
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);
|
||||
pub fn set_caret_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||
self.selection.set_caret(*position);
|
||||
self.reset_blink();
|
||||
self.push_event(EditorEvent::SelectionChanged);
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
}
|
||||
|
||||
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);
|
||||
pub fn extend_selection_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||
self.selection.extend_to(*position);
|
||||
self.reset_blink();
|
||||
self.push_event(EditorEvent::SelectionChanged);
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
}
|
||||
|
||||
pub fn update_blink(&mut self, timestamp_ms: f64) {
|
||||
@@ -264,41 +238,17 @@ impl TextEditorState {
|
||||
self.last_blink_time = 0.0;
|
||||
}
|
||||
|
||||
pub fn push_event(&mut self, event: EditorEvent) {
|
||||
pub fn push_event(&mut self, event: TextEditorEvent) {
|
||||
if self.pending_events.last() != Some(&event) {
|
||||
self.pending_events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_event(&mut self) -> EditorEvent {
|
||||
self.pending_events.pop().unwrap_or(EditorEvent::None)
|
||||
pub fn poll_event(&mut self) -> TextEditorEvent {
|
||||
self.pending_events.pop().unwrap_or(TextEditorEvent::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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::math::{Matrix, Point, Rect};
|
||||
use crate::mem;
|
||||
use crate::shapes::{Paragraph, Shape, TextContent, Type, VerticalAlign};
|
||||
use crate::state::{TextCursor, TextSelection};
|
||||
use crate::shapes::{Paragraph, Shape, TextContent, TextPositionWithAffinity, Type, VerticalAlign};
|
||||
use crate::state::{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 = TextCursor::new(cursor.paragraph, new_offset);
|
||||
let new_cursor = TextPositionWithAffinity::new_without_affinity(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::EditorEvent::ContentChanged);
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::NeedsLayout);
|
||||
.push_event(crate::state::TextEditorEvent::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::EditorEvent::ContentChanged);
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::NeedsLayout);
|
||||
.push_event(crate::state::TextEditorEvent::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::EditorEvent::ContentChanged);
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::NeedsLayout);
|
||||
.push_event(crate::state::TextEditorEvent::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 = TextCursor::new(cursor.paragraph + 1, 0);
|
||||
let new_cursor = TextPositionWithAffinity::new_without_affinity(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::EditorEvent::ContentChanged);
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::NeedsLayout);
|
||||
.push_event(crate::state::TextEditorEvent::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::EditorEvent::SelectionChanged);
|
||||
.push_event(crate::state::TextEditorEvent::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.char_offset
|
||||
start.offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let range_end = if para_idx == end.paragraph {
|
||||
end.char_offset
|
||||
end.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.char_offset as u32;
|
||||
*buffer_ptr.add(1) = sel.anchor.offset as u32;
|
||||
*buffer_ptr.add(2) = sel.focus.paragraph as u32;
|
||||
*buffer_ptr.add(3) = sel.focus.char_offset as u32;
|
||||
*buffer_ptr.add(3) = sel.focus.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: &TextCursor, shape: &Shape) -> Option<Rect> {
|
||||
fn get_cursor_rect(text_content: &TextContent, cursor: &TextPositionWithAffinity, 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: &TextCursor, shape: &Shap
|
||||
let mut y_offset = valign_offset;
|
||||
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||
if idx == cursor.paragraph {
|
||||
let char_pos = cursor.char_offset;
|
||||
let char_pos = cursor.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.char_offset
|
||||
start.offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let range_end = if para_idx == end.paragraph {
|
||||
end.char_offset
|
||||
end.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(cursor: TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
|
||||
fn clamp_cursor(position: TextPositionWithAffinity, paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
|
||||
if paragraphs.is_empty() {
|
||||
return TextCursor::new(0, 0);
|
||||
return TextPositionWithAffinity::new_without_affinity(0, 0);
|
||||
}
|
||||
|
||||
let para_idx = cursor.paragraph.min(paragraphs.len() - 1);
|
||||
let para_idx = position.paragraph.min(paragraphs.len() - 1);
|
||||
let para_len = paragraph_char_count(¶graphs[para_idx]);
|
||||
let char_offset = cursor.char_offset.min(para_len);
|
||||
let char_offset = position.offset.min(para_len);
|
||||
|
||||
TextCursor::new(para_idx, char_offset)
|
||||
TextPositionWithAffinity::new_without_affinity(para_idx, char_offset)
|
||||
}
|
||||
|
||||
/// Move cursor left by one character.
|
||||
fn move_cursor_backward(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
|
||||
if cursor.char_offset > 0 {
|
||||
TextCursor::new(cursor.paragraph, cursor.char_offset - 1)
|
||||
fn move_cursor_backward(cursor: &TextPositionWithAffinity, paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
|
||||
if cursor.offset > 0 {
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, cursor.offset - 1)
|
||||
} else if cursor.paragraph > 0 {
|
||||
let prev_para = cursor.paragraph - 1;
|
||||
let char_count = paragraph_char_count(¶graphs[prev_para]);
|
||||
TextCursor::new(prev_para, char_count)
|
||||
TextPositionWithAffinity::new_without_affinity(prev_para, char_count)
|
||||
} else {
|
||||
*cursor
|
||||
}
|
||||
}
|
||||
|
||||
/// Move cursor right by one character.
|
||||
fn move_cursor_forward(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
|
||||
fn move_cursor_forward(cursor: &TextPositionWithAffinity, paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
|
||||
let para = ¶graphs[cursor.paragraph];
|
||||
let char_count = paragraph_char_count(para);
|
||||
|
||||
if cursor.char_offset < char_count {
|
||||
TextCursor::new(cursor.paragraph, cursor.char_offset + 1)
|
||||
if cursor.offset < char_count {
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, cursor.offset + 1)
|
||||
} else if cursor.paragraph < paragraphs.len() - 1 {
|
||||
TextCursor::new(cursor.paragraph + 1, 0)
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph + 1, 0)
|
||||
} else {
|
||||
*cursor
|
||||
}
|
||||
@@ -955,52 +955,52 @@ fn move_cursor_forward(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCur
|
||||
|
||||
/// Move cursor up by one line.
|
||||
fn move_cursor_up(
|
||||
cursor: &TextCursor,
|
||||
cursor: &TextPositionWithAffinity,
|
||||
paragraphs: &[Paragraph],
|
||||
_text_content: &TextContent,
|
||||
_shape: &Shape,
|
||||
) -> TextCursor {
|
||||
) -> TextPositionWithAffinity {
|
||||
// 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(¶graphs[prev_para]);
|
||||
let new_offset = cursor.char_offset.min(char_count);
|
||||
TextCursor::new(prev_para, new_offset)
|
||||
let new_offset = cursor.offset.min(char_count);
|
||||
TextPositionWithAffinity::new_without_affinity(prev_para, new_offset)
|
||||
} else {
|
||||
TextCursor::new(cursor.paragraph, 0)
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Move cursor down by one line.
|
||||
fn move_cursor_down(
|
||||
cursor: &TextCursor,
|
||||
cursor: &TextPositionWithAffinity,
|
||||
paragraphs: &[Paragraph],
|
||||
_text_content: &TextContent,
|
||||
_shape: &Shape,
|
||||
) -> TextCursor {
|
||||
) -> TextPositionWithAffinity {
|
||||
// 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(¶graphs[next_para]);
|
||||
let new_offset = cursor.char_offset.min(char_count);
|
||||
TextCursor::new(next_para, new_offset)
|
||||
let new_offset = cursor.offset.min(char_count);
|
||||
TextPositionWithAffinity::new_without_affinity(next_para, new_offset)
|
||||
} else {
|
||||
let char_count = paragraph_char_count(¶graphs[cursor.paragraph]);
|
||||
TextCursor::new(cursor.paragraph, char_count)
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, char_count)
|
||||
}
|
||||
}
|
||||
|
||||
/// Move cursor to start of current line.
|
||||
fn move_cursor_line_start(cursor: &TextCursor, _paragraphs: &[Paragraph]) -> TextCursor {
|
||||
fn move_cursor_line_start(cursor: &TextPositionWithAffinity, _paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
|
||||
// TODO: Implement proper line-start using line metrics
|
||||
TextCursor::new(cursor.paragraph, 0)
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, 0)
|
||||
}
|
||||
|
||||
/// Move cursor to end of current line.
|
||||
fn move_cursor_line_end(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
|
||||
fn move_cursor_line_end(cursor: &TextPositionWithAffinity, paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
|
||||
// TODO: Implement proper line-end using line metrics
|
||||
let char_count = paragraph_char_count(¶graphs[cursor.paragraph]);
|
||||
TextCursor::new(cursor.paragraph, char_count)
|
||||
TextPositionWithAffinity::new_without_affinity(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: &TextCursor,
|
||||
cursor: &TextPositionWithAffinity,
|
||||
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.char_offset)?;
|
||||
let (span_idx, offset_in_span) = find_span_at_offset(para, cursor.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.char_offset + text.chars().count())
|
||||
Some(cursor.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.char_offset,
|
||||
end.char_offset,
|
||||
start.offset,
|
||||
end.offset,
|
||||
);
|
||||
} else {
|
||||
let start_para_len = paragraph_char_count(¶graphs[start.paragraph]);
|
||||
delete_range_in_paragraph(
|
||||
&mut paragraphs[start.paragraph],
|
||||
start.char_offset,
|
||||
start.offset,
|
||||
start_para_len,
|
||||
);
|
||||
|
||||
delete_range_in_paragraph(&mut paragraphs[end.paragraph], 0, end.char_offset);
|
||||
delete_range_in_paragraph(&mut paragraphs[end.paragraph], 0, end.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: &TextCursor) -> Option<TextCursor> {
|
||||
if cursor.char_offset > 0 {
|
||||
fn delete_char_before(text_content: &mut TextContent, cursor: &TextPositionWithAffinity) -> Option<TextPositionWithAffinity> {
|
||||
if cursor.offset > 0 {
|
||||
let paragraphs = text_content.paragraphs_mut();
|
||||
let para = &mut paragraphs[cursor.paragraph];
|
||||
let delete_pos = cursor.char_offset - 1;
|
||||
delete_range_in_paragraph(para, delete_pos, cursor.char_offset);
|
||||
Some(TextCursor::new(cursor.paragraph, delete_pos))
|
||||
let delete_pos = cursor.offset - 1;
|
||||
delete_range_in_paragraph(para, delete_pos, cursor.offset);
|
||||
Some(TextPositionWithAffinity::new_without_affinity(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: &TextCursor) -> Op
|
||||
|
||||
paragraphs.remove(cursor.paragraph);
|
||||
|
||||
Some(TextCursor::new(prev_para_idx, prev_para_len))
|
||||
Some(TextPositionWithAffinity::new_without_affinity(prev_para_idx, prev_para_len))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the character after the cursor.
|
||||
fn delete_char_after(text_content: &mut TextContent, cursor: &TextCursor) {
|
||||
fn delete_char_after(text_content: &mut TextContent, cursor: &TextPositionWithAffinity) {
|
||||
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: &TextCursor) {
|
||||
|
||||
let para_len = paragraph_char_count(¶graphs[cursor.paragraph]);
|
||||
|
||||
if cursor.char_offset < para_len {
|
||||
if cursor.offset < para_len {
|
||||
let para = &mut paragraphs[cursor.paragraph];
|
||||
delete_range_in_paragraph(para, cursor.char_offset, cursor.char_offset + 1);
|
||||
delete_range_in_paragraph(para, cursor.offset, cursor.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: &TextCursor) {
|
||||
}
|
||||
|
||||
/// Split a paragraph at the cursor position. Returns true if split was successful.
|
||||
fn split_paragraph_at_cursor(text_content: &mut TextContent, cursor: &TextCursor) -> bool {
|
||||
fn split_paragraph_at_cursor(text_content: &mut TextContent, cursor: &TextPositionWithAffinity) -> 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: &TextCursor
|
||||
|
||||
let para = ¶graphs[cursor.paragraph];
|
||||
|
||||
let Some((span_idx, offset_in_span)) = find_span_at_offset(para, cursor.char_offset) else {
|
||||
let Some((span_idx, offset_in_span)) = find_span_at_offset(para, cursor.offset) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user