mirror of
https://github.com/penpot/penpot.git
synced 2025-05-06 11:05:53 +02:00
✨ Write shapes directly to wasm memory
This commit is contained in:
parent
29e0964ebc
commit
4623f36042
9 changed files with 186 additions and 139 deletions
|
@ -158,22 +158,22 @@
|
||||||
y (dm/get-prop rect :y)
|
y (dm/get-prop rect :y)
|
||||||
w (dm/get-prop rect :width)
|
w (dm/get-prop rect :width)
|
||||||
h (dm/get-prop rect :height)]
|
h (dm/get-prop rect :height)]
|
||||||
(rc/assoc! rect
|
(assoc rect
|
||||||
:x1 x
|
:x1 x
|
||||||
:y1 y
|
:y1 y
|
||||||
:x2 (+ x w)
|
:x2 (+ x w)
|
||||||
:y2 (+ y h)))
|
:y2 (+ y h)))
|
||||||
|
|
||||||
:corners
|
:corners
|
||||||
(let [x1 (dm/get-prop rect :x1)
|
(let [x1 (dm/get-prop rect :x1)
|
||||||
y1 (dm/get-prop rect :y1)
|
y1 (dm/get-prop rect :y1)
|
||||||
x2 (dm/get-prop rect :x2)
|
x2 (dm/get-prop rect :x2)
|
||||||
y2 (dm/get-prop rect :y2)]
|
y2 (dm/get-prop rect :y2)]
|
||||||
(rc/assoc! rect
|
(assoc rect
|
||||||
:x (mth/min x1 x2)
|
:x (mth/min x1 x2)
|
||||||
:y (mth/min y1 y2)
|
:y (mth/min y1 y2)
|
||||||
:width (mth/abs (- x2 x1))
|
:width (mth/abs (- x2 x1))
|
||||||
:height (mth/abs (- y2 y1))))))
|
:height (mth/abs (- y2 y1))))))
|
||||||
|
|
||||||
(defn close-rect?
|
(defn close-rect?
|
||||||
[rect1 rect2]
|
[rect1 rect2]
|
||||||
|
|
|
@ -403,30 +403,30 @@
|
||||||
nil)))
|
nil)))
|
||||||
~rsym)))
|
~rsym)))
|
||||||
|
|
||||||
;; (defmacro clone
|
(defmacro clone
|
||||||
;; [ssym]
|
[ssym]
|
||||||
;; (if (:ns &env)
|
(if (:ns &env)
|
||||||
;; `(cljs.core/clone ~ssym)
|
`(cljs.core/clone ~ssym)
|
||||||
;; ssym))
|
ssym))
|
||||||
|
|
||||||
;; (defmacro assoc!
|
(defmacro assoc!
|
||||||
;; "A record specific update operation"
|
"A record specific update operation"
|
||||||
;; [ssym & pairs]
|
[ssym & pairs]
|
||||||
;; (if (:ns &env)
|
(if (:ns &env)
|
||||||
;; (let [pairs (partition-all 2 pairs)]
|
(let [pairs (partition-all 2 pairs)]
|
||||||
;; `(-> ~ssym
|
`(-> ~ssym
|
||||||
;; ~@(map (fn [[ks vs]]
|
~@(map (fn [[ks vs]]
|
||||||
;; `(cljs.core/-assoc! ~ks ~vs))
|
`(cljs.core/-assoc! ~ks ~vs))
|
||||||
;; pairs)))
|
pairs)))
|
||||||
;; `(assoc ~ssym ~@pairs)))
|
`(assoc ~ssym ~@pairs)))
|
||||||
|
|
||||||
;; (defmacro update!
|
(defmacro update!
|
||||||
;; "A record specific update operation"
|
"A record specific update operation"
|
||||||
;; [ssym ksym f & params]
|
[ssym ksym f & params]
|
||||||
;; (if (:ns &env)
|
(if (:ns &env)
|
||||||
;; (let [ssym (with-meta ssym {:tag 'js})]
|
(let [ssym (with-meta ssym {:tag 'js})]
|
||||||
;; `(cljs.core/assoc! ~ssym ~ksym (~f (. ~ssym ~(property-symbol ksym)) ~@params)))
|
`(cljs.core/assoc! ~ssym ~ksym (~f (. ~ssym ~(property-symbol ksym)) ~@params)))
|
||||||
;; `(update ~ssym ~ksym ~f ~@params)))
|
`(update ~ssym ~ksym ~f ~@params)))
|
||||||
|
|
||||||
(defmacro define-properties!
|
(defmacro define-properties!
|
||||||
"Define properties in the prototype with `.defineProperty`"
|
"Define properties in the prototype with `.defineProperty`"
|
||||||
|
|
|
@ -19,12 +19,12 @@
|
||||||
[app.common.schema.generators :as sg]
|
[app.common.schema.generators :as sg]
|
||||||
[app.common.transit :as t]
|
[app.common.transit :as t]
|
||||||
[app.common.types.color :as ctc]
|
[app.common.types.color :as ctc]
|
||||||
[app.common.types.shape.impl :as impl]
|
|
||||||
[app.common.types.grid :as ctg]
|
[app.common.types.grid :as ctg]
|
||||||
[app.common.types.plugins :as ctpg]
|
[app.common.types.plugins :as ctpg]
|
||||||
[app.common.types.shape.attrs :refer [default-color]]
|
[app.common.types.shape.attrs :refer [default-color]]
|
||||||
[app.common.types.shape.blur :as ctsb]
|
[app.common.types.shape.blur :as ctsb]
|
||||||
[app.common.types.shape.export :as ctse]
|
[app.common.types.shape.export :as ctse]
|
||||||
|
[app.common.types.shape.impl :as impl]
|
||||||
[app.common.types.shape.interactions :as ctsi]
|
[app.common.types.shape.interactions :as ctsi]
|
||||||
[app.common.types.shape.layout :as ctsl]
|
[app.common.types.shape.layout :as ctsl]
|
||||||
[app.common.types.shape.path :as ctsp]
|
[app.common.types.shape.path :as ctsp]
|
||||||
|
|
|
@ -10,18 +10,20 @@
|
||||||
[app.common.colors :as clr]
|
[app.common.colors :as clr]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.geom.rect :as grc]
|
||||||
[app.common.record :as cr]
|
[app.common.record :as cr]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.schema.generators :as sg]
|
[app.common.schema.generators :as sg]
|
||||||
[app.common.transit :as t]
|
[app.common.transit :as t]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.common.geom.rect :as grc]
|
|
||||||
[cuerdas.core :as str]
|
|
||||||
[clojure.core :as c]
|
[clojure.core :as c]
|
||||||
[clojure.set :as set]))
|
[clojure.set :as set]
|
||||||
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
(def ArrayBuffer js/ArrayBuffer)
|
#?(:cljs
|
||||||
(def Float32Array js/Float32Array)
|
(do
|
||||||
|
(def ArrayBuffer js/ArrayBuffer)
|
||||||
|
(def Float32Array js/Float32Array)))
|
||||||
|
|
||||||
(cr/defrecord Shape [id name type x y width height rotation selrect points
|
(cr/defrecord Shape [id name type x y width height rotation selrect points
|
||||||
transform transform-inverse parent-id frame-id flip-x flip-y])
|
transform transform-inverse parent-id frame-id flip-x flip-y])
|
||||||
|
@ -49,77 +51,77 @@
|
||||||
;; (let [bf32 (clone-float32-array buffer)]
|
;; (let [bf32 (clone-float32-array buffer)]
|
||||||
;; (ShapeWithBuffer. bf32 delegate)))
|
;; (ShapeWithBuffer. bf32 delegate)))
|
||||||
|
|
||||||
IWithMeta
|
IWithMeta
|
||||||
(-with-meta [coll meta]
|
(-with-meta [coll meta]
|
||||||
(ShapeWithBuffer. buffer (with-meta delegate meta)))
|
(ShapeWithBuffer. buffer (with-meta delegate meta)))
|
||||||
|
|
||||||
IMeta
|
IMeta
|
||||||
(-meta [coll] (meta delegate))
|
(-meta [coll] (meta delegate))
|
||||||
|
|
||||||
ICollection
|
ICollection
|
||||||
(-conj [coll entry]
|
(-conj [coll entry]
|
||||||
(impl-conj coll entry))
|
(impl-conj coll entry))
|
||||||
|
|
||||||
IEquiv
|
IEquiv
|
||||||
(-equiv [coll other]
|
(-equiv [coll other]
|
||||||
(c/equiv-map coll other))
|
(c/equiv-map coll other))
|
||||||
|
|
||||||
IHash
|
IHash
|
||||||
(-hash [coll] (hash (into {} coll)))
|
(-hash [coll] (hash (into {} coll)))
|
||||||
|
|
||||||
ISequential
|
ISequential
|
||||||
|
|
||||||
ISeqable
|
ISeqable
|
||||||
(-seq [coll]
|
(-seq [coll]
|
||||||
(cons (find coll :selrect)
|
(cons (find coll :selrect)
|
||||||
(seq delegate)))
|
(seq delegate)))
|
||||||
|
|
||||||
ICounted
|
ICounted
|
||||||
(-count [coll]
|
(-count [coll]
|
||||||
(+ 1 (count delegate)))
|
(+ 1 (count delegate)))
|
||||||
|
|
||||||
ILookup
|
ILookup
|
||||||
(-lookup [coll k]
|
(-lookup [coll k]
|
||||||
(-lookup coll k nil))
|
(-lookup coll k nil))
|
||||||
|
|
||||||
(-lookup [coll k not-found]
|
(-lookup [coll k not-found]
|
||||||
(if (= k :selrect)
|
(if (= k :selrect)
|
||||||
(read-selrect buffer)
|
(read-selrect buffer)
|
||||||
(c/-lookup delegate k not-found)))
|
(c/-lookup delegate k not-found)))
|
||||||
|
|
||||||
IFind
|
IFind
|
||||||
(-find [coll k]
|
(-find [coll k]
|
||||||
(if (= k :selrect)
|
(if (= k :selrect)
|
||||||
(c/MapEntry. k (read-selrect buffer) nil) ; Replace with lazy MapEntry
|
(c/MapEntry. k (read-selrect buffer) nil) ; Replace with lazy MapEntry
|
||||||
(c/-find delegate k)))
|
(c/-find delegate k)))
|
||||||
|
|
||||||
IAssociative
|
IAssociative
|
||||||
(-assoc [coll k v]
|
(-assoc [coll k v]
|
||||||
(impl-assoc coll k v))
|
(impl-assoc coll k v))
|
||||||
|
|
||||||
(-contains-key? [coll k]
|
(-contains-key? [coll k]
|
||||||
(or (= k :selrect)
|
(or (= k :selrect)
|
||||||
(contains? delegate k)))
|
(contains? delegate k)))
|
||||||
|
|
||||||
IMap
|
IMap
|
||||||
(-dissoc [coll k]
|
(-dissoc [coll k]
|
||||||
(impl-dissoc coll k))
|
(impl-dissoc coll k))
|
||||||
|
|
||||||
IFn
|
IFn
|
||||||
(-invoke [coll k]
|
(-invoke [coll k]
|
||||||
(-lookup coll k))
|
(-lookup coll k))
|
||||||
|
|
||||||
(-invoke [coll k not-found]
|
(-invoke [coll k not-found]
|
||||||
(-lookup coll k not-found))
|
(-lookup coll k not-found))
|
||||||
|
|
||||||
IPrintWithWriter
|
IPrintWithWriter
|
||||||
(-pr-writer [coll writer opts]
|
(-pr-writer [coll writer opts]
|
||||||
(-write writer (str "#penpot/shape " (:id delegate))))))
|
(-write writer (str "#penpot/shape " (:id delegate))))))
|
||||||
|
|
||||||
(defn shape?
|
(defn shape?
|
||||||
[o]
|
[o]
|
||||||
(or (instance? Shape o)
|
(or (instance? Shape o)
|
||||||
(instance? ShapeWithBuffer o)))
|
#?(:cljs (instance? ShapeWithBuffer o))))
|
||||||
|
|
||||||
;; --- SHAPE IMPL
|
;; --- SHAPE IMPL
|
||||||
|
|
||||||
|
@ -211,12 +213,12 @@
|
||||||
:rfn #?(:cljs create-shape
|
:rfn #?(:cljs create-shape
|
||||||
:clj map->Shape)})
|
:clj map->Shape)})
|
||||||
|
|
||||||
(t/add-handlers!
|
#?(:cljs (t/add-handlers!
|
||||||
{:id "shape"
|
{:id "shape"
|
||||||
:class ShapeWithBuffer
|
:class ShapeWithBuffer
|
||||||
:wfn #(into {} %)
|
:wfn #(into {} %)
|
||||||
:rfn #?(:cljs create-shape
|
:rfn #?(:cljs create-shape
|
||||||
:clj map->Shape)})
|
:clj map->Shape)}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
(cr/update! :x2 + (:x delta))
|
(cr/update! :x2 + (:x delta))
|
||||||
(cr/update! :y2 + (:y delta)))
|
(cr/update! :y2 + (:y delta)))
|
||||||
selrect (if ^boolean space?
|
selrect (if ^boolean space?
|
||||||
(-> selrect
|
(-> (cr/clone selrect)
|
||||||
(cr/update! :x1 + (:x delta))
|
(cr/update! :x1 + (:x delta))
|
||||||
(cr/update! :y1 + (:y delta)))
|
(cr/update! :y1 + (:y delta)))
|
||||||
selrect)]
|
selrect)]
|
||||||
|
|
|
@ -134,13 +134,13 @@
|
||||||
hover-top-frame-id (mf/use-state nil)
|
hover-top-frame-id (mf/use-state nil)
|
||||||
frame-hover (mf/use-state nil)
|
frame-hover (mf/use-state nil)
|
||||||
active-frames (mf/use-state #{})
|
active-frames (mf/use-state #{})
|
||||||
|
canvas-init? (mf/use-state false)
|
||||||
|
|
||||||
;; REFS
|
;; REFS
|
||||||
[viewport-ref
|
[viewport-ref
|
||||||
on-viewport-ref] (create-viewport-ref)
|
on-viewport-ref] (create-viewport-ref)
|
||||||
|
|
||||||
canvas-ref (mf/use-ref nil)
|
canvas-ref (mf/use-ref nil)
|
||||||
canvas-init (mf/use-ref false)
|
|
||||||
|
|
||||||
;; VARS
|
;; VARS
|
||||||
disable-paste (mf/use-var false)
|
disable-paste (mf/use-var false)
|
||||||
|
@ -277,19 +277,22 @@
|
||||||
(when ^boolean render.wasm/enabled?
|
(when ^boolean render.wasm/enabled?
|
||||||
(mf/with-effect []
|
(mf/with-effect []
|
||||||
(time (when-let [canvas (mf/ref-val canvas-ref)]
|
(time (when-let [canvas (mf/ref-val canvas-ref)]
|
||||||
(->> render.wasm/module
|
(->> render.wasm/module
|
||||||
(p/fmap (fn [ready?]
|
(p/fmap (fn [ready?]
|
||||||
(when ready?
|
(when ready?
|
||||||
(mf/set-ref-val! canvas-init true)
|
(reset! canvas-init? true)
|
||||||
(render.wasm/assign-canvas canvas)))))
|
(render.wasm/assign-canvas canvas)))))
|
||||||
(fn []
|
(fn []
|
||||||
(render.wasm/clear-canvas)))))
|
(render.wasm/clear-canvas)))))
|
||||||
|
|
||||||
(mf/with-effect [vbox objects-modified]
|
(mf/with-effect [objects-modified canvas-init?]
|
||||||
(let [sem (when (mf/ref-val canvas-init)
|
(when @canvas-init?
|
||||||
(render.wasm/draw-objects objects-modified zoom vbox))]
|
(render.wasm/set-objects objects-modified)
|
||||||
(partial render.wasm/cancel-draw sem)))
|
(render.wasm/draw-objects zoom vbox)))
|
||||||
)
|
(mf/with-effect [vbox canvas-init?]
|
||||||
|
(let [frame-id (when @canvas-init? (do
|
||||||
|
(render.wasm/draw-objects zoom vbox)))]
|
||||||
|
(partial render.wasm/cancel-draw frame-id))))
|
||||||
|
|
||||||
(hooks/setup-dom-events zoom disable-paste in-viewport? read-only? drawing-tool drawing-path?)
|
(hooks/setup-dom-events zoom disable-paste in-viewport? read-only? drawing-tool drawing-path?)
|
||||||
(hooks/setup-viewport-size vport viewport-ref)
|
(hooks/setup-viewport-size vport viewport-ref)
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"A WASM based render API"
|
"A WASM based render API"
|
||||||
(:require
|
(:require
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.files.helpers :as cfh]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[promesa.core :as p]))
|
[promesa.core :as p]))
|
||||||
|
|
||||||
|
@ -17,41 +18,31 @@
|
||||||
(defonce ^:dynamic internal-module #js {})
|
(defonce ^:dynamic internal-module #js {})
|
||||||
(defonce ^:dynamic internal-gpu-state #js {})
|
(defonce ^:dynamic internal-gpu-state #js {})
|
||||||
|
|
||||||
(defn draw-objects
|
(defn set-objects [objects]
|
||||||
[objects zoom vbox]
|
(let [shapes-buffer (unchecked-get internal-module "_shapes_buffer")
|
||||||
(let [draw-rect (unchecked-get internal-module "_draw_rect")
|
heap (unchecked-get internal-module "HEAPF32")
|
||||||
translate (unchecked-get internal-module "_translate")
|
;; size *in bytes* for each shapes::Shape
|
||||||
reset-canvas (unchecked-get internal-module "_reset_canvas")
|
rect-size 16
|
||||||
scale (unchecked-get internal-module "_scale")
|
;; TODO: remove the `take` once we have the dynamic data structure in Rust
|
||||||
flush (unchecked-get internal-module "_flush")
|
supported-shapes (take 2048 (filter #(not (cfh/root? %)) (vals objects)))
|
||||||
gpu-state internal-gpu-state]
|
mem (js/Float32Array. (.-buffer heap) (shapes-buffer) (* rect-size (count supported-shapes)))]
|
||||||
|
(run! (fn [[shape index]]
|
||||||
|
(.set mem (.-buffer shape) (* index rect-size)))
|
||||||
|
(zipmap supported-shapes (range)))))
|
||||||
|
|
||||||
|
(defn draw-objects
|
||||||
|
[zoom vbox]
|
||||||
|
(let [draw-all-shapes (unchecked-get internal-module "_draw_all_shapes")]
|
||||||
(js/requestAnimationFrame
|
(js/requestAnimationFrame
|
||||||
(fn []
|
(fn []
|
||||||
(reset-canvas gpu-state)
|
(let [pan-x (- (dm/get-prop vbox :x))
|
||||||
(scale gpu-state zoom zoom)
|
pan-y (- (dm/get-prop vbox :y))]
|
||||||
|
(draw-all-shapes internal-gpu-state zoom pan-x pan-y))))))
|
||||||
(let [x (dm/get-prop vbox :x)
|
|
||||||
y (dm/get-prop vbox :y)]
|
|
||||||
(translate gpu-state (- x) (- y)))
|
|
||||||
|
|
||||||
(run! (fn [shape]
|
|
||||||
;; (js/console.log "render-shape" (.-buffer shape))
|
|
||||||
(let [selrect (dm/get-prop shape :selrect)
|
|
||||||
x1 (dm/get-prop selrect :x1)
|
|
||||||
y1 (dm/get-prop selrect :y1)
|
|
||||||
x2 (dm/get-prop selrect :x2)
|
|
||||||
y2 (dm/get-prop selrect :y2)]
|
|
||||||
;; (prn (:id shape) selrect)
|
|
||||||
(draw-rect gpu-state x1 y1 x2 y2)))
|
|
||||||
(vals objects))
|
|
||||||
|
|
||||||
(flush gpu-state)))))
|
|
||||||
|
|
||||||
(defn cancel-draw
|
(defn cancel-draw
|
||||||
[sem]
|
[frame-id]
|
||||||
(when (some? sem)
|
(when (some? frame-id)
|
||||||
(js/cancelAnimationFrame sem)))
|
(js/cancelAnimationFrame frame-id)))
|
||||||
|
|
||||||
(def ^:private canvas-options
|
(def ^:private canvas-options
|
||||||
#js {:antialias true
|
#js {:antialias true
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod render;
|
pub mod render;
|
||||||
|
pub mod shapes;
|
||||||
|
|
||||||
use skia_safe as skia;
|
use skia_safe as skia;
|
||||||
|
|
||||||
|
@ -33,6 +34,19 @@ pub unsafe extern "C" fn draw_rect(state: *mut State, x1: f32, y1: f32, x2: f32,
|
||||||
render::render_rect(&mut state.surface, r, skia::Color::RED);
|
render::render_rect(&mut state.surface, r, skia::Color::RED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn draw_all_shapes(state: *mut State, zoom: f32, pan_x: f32, pan_y: f32) {
|
||||||
|
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
|
||||||
|
|
||||||
|
reset_canvas(state);
|
||||||
|
scale(state, zoom, zoom);
|
||||||
|
translate(state, pan_x, pan_y);
|
||||||
|
|
||||||
|
shapes::draw_all(state);
|
||||||
|
|
||||||
|
flush(state);
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn flush(state: *mut State) {
|
pub unsafe extern "C" fn flush(state: *mut State) {
|
||||||
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
|
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
|
||||||
|
@ -60,6 +74,12 @@ pub unsafe extern "C" fn reset_canvas(state: *mut State) {
|
||||||
flush(state);
|
flush(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn shapes_buffer() -> *mut shapes::Shape {
|
||||||
|
let ptr = shapes::SHAPES_BUFFER.as_mut_ptr();
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
render::init_gl();
|
render::init_gl();
|
||||||
}
|
}
|
||||||
|
|
31
render-wasm/src/shapes.rs
Normal file
31
render-wasm/src/shapes.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use crate::render::{render_rect, State};
|
||||||
|
use skia_safe as skia;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Selrect {
|
||||||
|
pub x1: f32,
|
||||||
|
pub y1: f32,
|
||||||
|
pub x2: f32,
|
||||||
|
pub y2: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Shape = Selrect; // temp
|
||||||
|
|
||||||
|
pub static mut SHAPES_BUFFER: [Shape; 2048] = [Selrect {
|
||||||
|
x1: 0.0,
|
||||||
|
y1: 0.0,
|
||||||
|
x2: 0.0,
|
||||||
|
y2: 0.0,
|
||||||
|
}; 2048];
|
||||||
|
|
||||||
|
pub(crate) fn draw_all(state: &mut State) {
|
||||||
|
let shapes;
|
||||||
|
unsafe {
|
||||||
|
shapes = SHAPES_BUFFER.iter();
|
||||||
|
}
|
||||||
|
|
||||||
|
for shape in shapes {
|
||||||
|
let r = skia::Rect::new(shape.x1, shape.y1, shape.x2, shape.y2);
|
||||||
|
render_rect(&mut state.surface, r, skia::Color::RED);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue