diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 54dde7688d..467ee7c731 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -216,6 +216,18 @@ on-frame-leave (actions/on-frame-leave frame-hover) on-frame-select (actions/on-frame-select selected read-only?) + ;; Text Editor Event Handlers + on-text-keydown (fn [event] + (when (and text-editing? (.-key event)) + (.preventDefault event) + (wasm.api/handle-text-keydown (.-key event)))) + on-text-mousedown (fn [event] + (when text-editing? + (let [rect (.getBoundingClientRect (.-currentTarget event)) + x (- (.-clientX event) (.-left rect)) + y (- (.-clientY event) (.-top rect))] + (wasm.api/handle-text-mousedown x y)))) + disable-events? (contains? layout :comments) show-comments? (= drawing-tool :comments) show-cursor-tooltip? tooltip @@ -231,8 +243,9 @@ show-pixel-grid? (and (contains? layout :show-pixel-grid) (>= zoom 8)) - show-text-editor? (and editing-shape (= :text (:type editing-shape))) + ;; show-text-editor? (and editing-shape (= :text (:type editing-shape))) + show-text-editor? false hover-grid? (and (some? @hover-top-frame-id) (ctl/grid-layout? objects @hover-top-frame-id)) @@ -419,7 +432,9 @@ :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave :on-pointer-move on-pointer-move - :on-pointer-up on-pointer-up} + :on-pointer-up on-pointer-up + :on-key-down on-text-keydown + :on-mouse-down on-text-mousedown} [:defs ;; This clip is so the handlers are not over the rulers diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index ba5525791e..e0fe917b33 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1212,6 +1212,7 @@ (defn init-canvas-context [canvas] + (let [gl (unchecked-get wasm/internal-module "GL") flags (debug-flags) context-id (if (dbg/enabled? :wasm-gl-context-init-error) "fail" "webgl2") @@ -1258,6 +1259,19 @@ (h/call wasm/internal-module "_hide_grid") (request-render "clear-grid")) +;; Text Editor Functions +(defn handle-text-keydown + [key] + (when (and wasm/internal-module key) + (h/call wasm/internal-module "_handle_keydown" key) + (request-render "text-editor-keydown"))) + +(defn handle-text-mousedown + [x y] + (when (and wasm/internal-module x y) + (h/call wasm/internal-module "_handle_mousedown" x y) + (request-render "text-editor-mousedown"))) + (defn get-grid-coords [position] (let [offset (h/call wasm/internal-module diff --git a/render-wasm/Cargo.lock b/render-wasm/Cargo.lock index 9a9d050849..718450b0bb 100644 --- a/render-wasm/Cargo.lock +++ b/render-wasm/Cargo.lock @@ -432,6 +432,7 @@ dependencies = [ "indexmap", "macros", "skia-safe", + "text_editor", "uuid", ] @@ -577,6 +578,14 @@ dependencies = [ "xattr", ] +[[package]] +name = "text_editor" +version = "0.1.0" +dependencies = [ + "skia-safe", + "wasm-bindgen", +] + [[package]] name = "toml" version = "0.8.19" diff --git a/render-wasm/Cargo.toml b/render-wasm/Cargo.toml index 82cde41199..67ad816dcb 100644 --- a/render-wasm/Cargo.toml +++ b/render-wasm/Cargo.toml @@ -32,6 +32,7 @@ skia-safe = { version = "0.87.0", default-features = false, features = [ "binary-cache", "webp", ] } +text_editor = { path = "src/text_editor" } uuid = { version = "1.11.0", features = ["v4", "js"] } [profile.release] diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 3e19967a85..2c7c4f172a 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -650,6 +650,38 @@ pub extern "C" fn set_modifiers() { }); } +#[no_mangle] +pub extern "C" fn handle_keydown(key_ptr: *const std::os::raw::c_char) { + if key_ptr.is_null() { + return; + } + + let key = unsafe { + match std::ffi::CStr::from_ptr(key_ptr).to_str() { + Ok(s) => s, + Err(_) => return, // Invalid UTF-8, skip + } + }; + + with_state_mut!(state, { + state.render_state.text_editor.handle_keydown(key); + }); + render_sync(); +} + +#[no_mangle] +pub extern "C" fn handle_mousedown(x: f32, y: f32) { + // Basic sanity checks + if !x.is_finite() || !y.is_finite() { + return; + } + + with_state_mut!(state, { + state.render_state.text_editor.handle_mousedown(x, y); + }); + render_sync(); +} + fn main() { #[cfg(target_arch = "wasm32")] init_gl!(); diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 3875da7f00..abe34f6535 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -215,6 +215,7 @@ pub(crate) struct RenderState { pub options: RenderOptions, pub surfaces: Surfaces, pub fonts: FontStore, + pub text_editor: ::text_editor::TextEditor, pub viewbox: Viewbox, pub cached_viewbox: Viewbox, pub cached_target_snapshot: Option, @@ -288,12 +289,14 @@ impl RenderState { let viewbox = Viewbox::new(width as f32, height as f32); let tiles = tiles::TileHashMap::new(); + let text_editor = ::text_editor::TextEditor::new(fonts.debug_font.clone()); RenderState { gpu_state: gpu_state.clone(), options: RenderOptions::default(), surfaces, fonts, + text_editor, viewbox, cached_viewbox: Viewbox::new(0., 0.), cached_target_snapshot: None, @@ -916,6 +919,8 @@ impl RenderState { ui::render(self, shapes); debug::render_wasm_label(self); + self.text_editor.render(self.surfaces.canvas(SurfaceId::Target)); + self.flush_and_submit(); } } @@ -1791,6 +1796,8 @@ impl RenderState { ui::render(self, tree); debug::render_wasm_label(self); + self.text_editor.render(self.surfaces.canvas(SurfaceId::Target)); + Ok(()) } diff --git a/render-wasm/src/render/fonts.rs b/render-wasm/src/render/fonts.rs index 917fe134a8..a153c82471 100644 --- a/render-wasm/src/render/fonts.rs +++ b/render-wasm/src/render/fonts.rs @@ -21,7 +21,7 @@ pub struct FontStore { font_mgr: FontMgr, font_provider: textlayout::TypefaceFontProvider, font_collection: textlayout::FontCollection, - debug_font: Font, + pub debug_font: Font, fallback_fonts: HashSet, } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 4257ab6da8..bd459c97fe 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -1,10 +1,8 @@ -use skia_safe::{self as skia, textlayout::FontCollection, Path, Point}; +use skia_safe::{self as skia, Path, Point, textlayout::FontCollection}; use std::collections::HashMap; mod shapes_pool; -mod text_editor; pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef}; -pub use text_editor::*; use crate::render::RenderState; use crate::shapes::Shape; @@ -20,7 +18,6 @@ use crate::shapes::modifiers::grid_layout::grid_cell_data; /// must not be shared between different Web Workers. pub(crate) struct State<'a> { pub render_state: RenderState, - pub text_editor_state: TextEditorState, pub current_id: Option, pub current_browser: u8, pub shapes: ShapesPool<'a>, @@ -28,9 +25,9 @@ pub(crate) struct State<'a> { impl<'a> State<'a> { pub fn new(width: i32, height: i32) -> Self { + let render_state = RenderState::new(width, height); State { - render_state: RenderState::new(width, height), - text_editor_state: TextEditorState::new(), + render_state, current_id: None, current_browser: 0, shapes: ShapesPool::new(), @@ -49,16 +46,6 @@ impl<'a> State<'a> { &self.render_state } - #[allow(dead_code)] - pub fn text_editor_state_mut(&mut self) -> &mut TextEditorState { - &mut self.text_editor_state - } - - #[allow(dead_code)] - pub fn text_editor_state(&self) -> &TextEditorState { - &self.text_editor_state - } - pub fn render_from_cache(&mut self) { self.render_state.render_from_cache(&self.shapes); } diff --git a/render-wasm/src/text_editor/Cargo.toml b/render-wasm/src/text_editor/Cargo.toml new file mode 100644 index 0000000000..e64761e57c --- /dev/null +++ b/render-wasm/src/text_editor/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "text_editor" +version = "0.1.0" +edition = "2021" + +[dependencies] +skia-safe = { version = "0.87.0", default-features = false, features = ["gl"] } +wasm-bindgen = "0.2" diff --git a/render-wasm/src/text_editor/src/events.rs b/render-wasm/src/text_editor/src/events.rs new file mode 100644 index 0000000000..7ff6788fa1 --- /dev/null +++ b/render-wasm/src/text_editor/src/events.rs @@ -0,0 +1,12 @@ + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn handle_keydown(key: String) { + // TODO: Handle keydown event +} + +#[wasm_bindgen] +pub fn handle_mousedown(x: f32, y: f32) { + // TODO: Handle mousedown event +} diff --git a/render-wasm/src/text_editor/src/lib.rs b/render-wasm/src/text_editor/src/lib.rs new file mode 100644 index 0000000000..b20dc02d68 --- /dev/null +++ b/render-wasm/src/text_editor/src/lib.rs @@ -0,0 +1,153 @@ +pub mod events; + +use skia_safe::{Canvas, Font, Paint, Point, Color}; + +pub struct TextEditor { + text: Vec, + font: Font, + cursor_pos: Point, +} + +impl TextEditor { + pub fn new(font: Font) -> Self { + TextEditor { + text: vec!["Hello, Skia!".to_string()], + font, + cursor_pos: Point::new(0.0, 0.0), + } + } + + pub fn render(&self, canvas: &Canvas) { + let mut paint = Paint::default(); + paint.set_color(Color::BLACK); + paint.set_anti_alias(true); + + for (i, line) in self.text.iter().enumerate() { + canvas.draw_str(line, (20.0, 20.0 + (i as f32 * 18.0)), &self.font, &paint); + } + + // Draw cursor - with bounds checking + let mut cursor_paint = Paint::default(); + cursor_paint.set_color(Color::BLACK); + cursor_paint.set_anti_alias(true); + + let y_idx = self.cursor_pos.y as usize; + let x_idx = self.cursor_pos.x as usize; + + if y_idx < self.text.len() { + let line = &self.text[y_idx]; + let safe_x_idx = x_idx.min(line.len()); + + let (x, _) = if safe_x_idx > 0 { + self.font.measure_str(&line[..safe_x_idx], None) + } else { + (0.0, skia_safe::Rect::new_empty()) + }; + + let cursor_rect = skia_safe::Rect::from_xywh( + 20.0 + x, + 20.0 + (y_idx as f32 * 18.0) - 18.0, + 1.0, + 18.0 + ); + canvas.draw_rect(cursor_rect, &cursor_paint); + } + } + + pub fn handle_keydown(&mut self, key: &str) { + if self.text.is_empty() { + self.text.push(String::new()); + } + + let y = self.cursor_pos.y as usize; + let x = self.cursor_pos.x as usize; + + if y >= self.text.len() { + return; + } + + match key { + "ArrowLeft" => { + if x > 0 { + self.cursor_pos.x -= 1.0; + } else if y > 0 { + self.cursor_pos.y -= 1.0; + self.cursor_pos.x = self.text[y - 1].len() as f32; + } + } + "ArrowRight" => { + if x < self.text[y].len() { + self.cursor_pos.x += 1.0; + } else if y < self.text.len() - 1 { + self.cursor_pos.y += 1.0; + self.cursor_pos.x = 0.0; + } + } + "ArrowUp" => { + if y > 0 { + self.cursor_pos.y -= 1.0; + let new_y = y - 1; + self.cursor_pos.x = self.cursor_pos.x.min(self.text[new_y].len() as f32); + } + } + "ArrowDown" => { + if y < self.text.len() - 1 { + self.cursor_pos.y += 1.0; + let new_y = y + 1; + self.cursor_pos.x = self.cursor_pos.x.min(self.text[new_y].len() as f32); + } + } + "Backspace" => { + if x > 0 { + self.text[y].remove(x - 1); + self.cursor_pos.x -= 1.0; + } else if y > 0 { + let line = self.text.remove(y); + self.cursor_pos.y -= 1.0; + self.cursor_pos.x = self.text[y - 1].len() as f32; + self.text[y - 1].push_str(&line); + } + } + "Enter" => { + let line = self.text[y].split_off(x); + self.text.insert(y + 1, line); + self.cursor_pos.y += 1.0; + self.cursor_pos.x = 0.0; + } + _ => { + if key.len() == 1 { + if let Some(ch) = key.chars().next() { + self.text[y].insert(x, ch); + self.cursor_pos.x += 1.0; + } + } + } + } + } + + pub fn handle_mousedown(&mut self, x: f32, y: f32) { + println!("@@@ Mouse down at: ({}, {})", x, y); + if self.text.is_empty() { + self.text.push(String::new()); + } + + let line_height = 18.0; + let y_pos = ((y - 20.0) / line_height).floor().max(0.0) as usize; + let y_pos = y_pos.min(self.text.len() - 1); + self.cursor_pos.y = y_pos as f32; + + let line = &self.text[y_pos]; + let mut closest_pos = 0; + let mut min_dist = f32::MAX; + + for i in 0..=line.len() { + let (width, _) = self.font.measure_str(&line[..i], None); + let dist = (x - (20.0 + width)).abs(); + if dist < min_dist { + min_dist = dist; + closest_pos = i; + } + } + self.cursor_pos.x = closest_pos as f32; + } +}