Browse Source

Several minor steps on the way to text cursors.

Kestrel 1 month ago
parent
commit
a90f9e65ef
9 changed files with 139 additions and 56 deletions
  1. 0 3
      examples/form_demo.rs
  2. 0 10
      examples/text_edits.rs
  3. 37 0
      src/input.rs
  4. 12 0
      src/layout.rs
  5. 2 0
      src/render/text.rs
  6. 5 5
      src/widget/button.rs
  7. 8 3
      src/widget/label.rs
  8. 61 33
      src/widget/text_edit.rs
  9. 14 2
      src/window.rs

+ 0 - 3
examples/form_demo.rs

@@ -44,9 +44,6 @@ impl Component for FormWindow {
     type Msg = ();
 
     fn process(&mut self, _msg: Self::Msg) -> Vec<Self::ParentMsg> {
-        let mut out = String::new();
-        patina::layout::dump_node_tree(self.root.layout_node(), &mut out);
-        std::fs::write("patina-form-example.layout_tree", out).expect("couldn't dump node tree");
         vec![UIControlMsg::Terminate]
     }
 }

+ 0 - 10
examples/text_edits.rs

@@ -49,16 +49,6 @@ impl Component for TextEditWindow {
     type Msg = TextEditMsg;
 
     fn process(&mut self, _msg: Self::Msg) -> Vec<Self::ParentMsg> {
-        let mut out = String::new();
-        patina::layout::dump_node_tree(self.root.layout_node(), &mut out);
-        std::fs::write("patina-form-example.layout_tree", out).expect("couldn't dump node tree");
-
-        let jsout = std::fs::File::create("patina-text-edit-example.json").unwrap();
-        patina::layout::dump_tree_json(
-            self.root.layout_node(),
-            &mut std::io::BufWriter::new(jsout),
-        )
-        .expect("couldn't dump node tree JSON");
         vec![UIControlMsg::Terminate]
     }
 }

+ 37 - 0
src/input.rs

@@ -49,6 +49,43 @@ pub enum MouseCheckStatus {
     Release,
 }
 
+impl MouseCheckStatus {
+    /// is the cursor outside the region?
+    pub fn is_idle(&self) -> bool {
+        *self == Self::Idle
+    }
+
+    /// did the cursor just enter the region?
+    pub fn is_enter(&self) -> bool {
+        *self == Self::Enter
+    }
+
+    /// did the cursor just leave the region?
+    pub fn is_leave(&self) -> bool {
+        *self == Self::Leave
+    }
+
+    /// is the cursor inside the region with no mouse buttons active?
+    pub fn is_hover(&self) -> bool {
+        *self == Self::Hover
+    }
+
+    /// is the cursor inside the region with a mouse button held?
+    pub fn is_hold(&self) -> bool {
+        *self == Self::Hold
+    }
+
+    /// is the cursor inside the region and the mouse button just pressed?
+    pub fn is_click(&self) -> bool {
+        *self == Self::Click
+    }
+
+    /// is the cursor inside the region and the mouse button just released?
+    pub fn is_release(&self) -> bool {
+        *self == Self::Release
+    }
+}
+
 #[derive(Default)]
 pub struct MouseState {
     pub pos: kahlo::math::PixelPoint,

+ 12 - 0
src/layout.rs

@@ -260,9 +260,13 @@ pub enum NodeBehaviour {
 #[derive(Clone)]
 pub enum ChildArrangement {
     Custom(std::rc::Rc<dyn arr::ArrangementCalculator>),
+    /// Node that wraps a single other node
     Container,
+    /// Node that arranges its children in a vertical column
     Column,
+    /// Node that arranges its children in a horizontal row
     Row,
+    /// Node that arranges its children in a 2D table using [`TableCell`]
     Table,
 }
 
@@ -353,10 +357,12 @@ impl LayoutNode {
         }
     }
 
+    /// Get the [`LayoutNodeID`] for this node.
     pub fn id(&self) -> LayoutNodeID {
         self.cache_key
     }
 
+    /// Acquire a reference to the [`LayoutCache`] this node belongs to.
     pub fn cache(&self) -> &Rc<LayoutCache> {
         &self.cache
     }
@@ -365,6 +371,7 @@ impl LayoutNode {
         self.cache.with_state(self.cache_key, f)
     }
 
+    /// Mark as requiring a relayout.
     pub fn relayout(&self) {
         self.cache.update_queue.borrow_mut().push(self.cache_key);
     }
@@ -393,6 +400,7 @@ impl LayoutNode {
         .unwrap_or(false)
     }
 
+    /// Mark as requiring a rerender
     pub fn render_needed(&self) {
         self.cache.render_queue.borrow_mut().push(self.cache_key);
     }
@@ -453,6 +461,7 @@ impl LayoutNode {
     }
     pub fn set_halign(&mut self, halign: HorizontalAlignment) -> &mut Self {
         self.halign = halign;
+        self.relayout();
         self
     }
     pub fn valign(&self) -> VerticalAlignment {
@@ -460,6 +469,7 @@ impl LayoutNode {
     }
     pub fn set_valign(&mut self, valign: VerticalAlignment) -> &mut Self {
         self.valign = valign;
+        self.relayout();
         self
     }
 
@@ -467,6 +477,7 @@ impl LayoutNode {
         self.margin
     }
     pub fn margin_mut(&mut self) -> &mut PixelSideOffsets {
+        self.relayout();
         &mut self.margin
     }
     pub fn margin_as_policy(&self) -> SizePolicy2D {
@@ -480,6 +491,7 @@ impl LayoutNode {
         &self.table_cell
     }
     pub fn set_table_cell(&mut self, cell: Option<TableCell>) {
+        self.relayout();
         self.table_cell = cell;
     }
 }

+ 2 - 0
src/render/text.rs

@@ -11,6 +11,7 @@ pub struct TextCache {
     text: String,
     font: Rc<fontdue::Font>,
     pxsize: f32,
+    x_offsets: Vec<u32>,
     rendered: RefCell<Option<kahlo::Alphamap>>,
 }
 
@@ -20,6 +21,7 @@ impl TextCache {
             text: String::new(),
             font,
             pxsize,
+            x_offsets: vec![],
             rendered: Default::default(),
         }
     }

+ 5 - 5
src/widget/button.rs

@@ -48,8 +48,7 @@ impl<C: Component> Button<C> {
             root_layout: layout,
             label_layout: {
                 let mut node = uih.new_layout_node();
-                node
-                    .set_width_policy(SizePolicy::fixed(0))
+                node.set_width_policy(SizePolicy::fixed(0))
                     .set_height_policy(SizePolicy::fixed(0))
                     .set_halign(HorizontalAlignment::Centre)
                     .set_valign(VerticalAlignment::Centre);
@@ -106,8 +105,8 @@ impl<C: Component> Widget<C> for Button<C> {
         _uih: &mut UIHandle,
         input_state: Option<&crate::input::InputState>,
     ) -> Vec<<C as Component>::Msg> {
-
-        self.text_cache.update_node_size_hint(&mut *self.label_layout);
+        self.text_cache
+            .update_node_size_hint(&mut *self.label_layout);
 
         let mut result: Vec<<C as Component>::Msg> = vec![];
         if let Some((istate, area)) = input_state.zip(self.root_layout.render_area()) {
@@ -152,7 +151,8 @@ impl<C: Component> Widget<C> for Button<C> {
             })
             .border()
             .finish();
-        target.with_node(&self.label_layout)
+        target
+            .with_node(&self.label_layout)
             .simple_text(&self.text_cache, ColourChoice::Foreground)
             .finish();
     }

+ 8 - 3
src/widget/label.rs

@@ -71,10 +71,15 @@ impl<C: Component> LayoutTreeNode<()> for Label<C> {
     fn current_node(&self) -> &LayoutNode {
         &self.root_layout
     }
-    fn child_count(&self) -> usize { 1 }
+    fn child_count(&self) -> usize {
+        1
+    }
     fn child(&self, ndx: usize) -> Option<LayoutNodeAccess<'_>> {
-        if ndx == 0 { Some(self.text_layout.access()) }
-        else { None }
+        if ndx == 0 {
+            Some(self.text_layout.access())
+        } else {
+            None
+        }
     }
 }
 

+ 61 - 33
src/widget/text_edit.rs

@@ -1,17 +1,25 @@
 use crate::{
-    input::{MouseButton, MouseCheckStatus},
-    layout::{LayoutNode, LayoutNodeAccess, LeafLayoutNode, SizePolicy},
+    input::MouseButton,
+    layout::{HorizontalAlignment, LayoutNode, LayoutNodeAccess, LayoutTreeNode, LeafLayoutNode, SizePolicy},
     prelude::*,
     render::{ColourChoice, RenderTarget, TextCache},
     ui::UIHandle,
 };
 use winit::keyboard::{Key, NamedKey};
 
+pub enum CursorPosition {
+    Start,
+    Index(usize),
+    End,
+}
+
 pub struct TextEdit<C: Component> {
-    lnode: LeafLayoutNode,
+    lnode: LayoutNode,
+    cursor_node: LeafLayoutNode,
     text: TextCache,
-    cursor_pos: Option<usize>,
+    cursor_pos: CursorPosition,
     onchange: Option<Box<dyn Fn(&str) -> Option<C::Msg>>>,
+    focused: bool,
 }
 
 impl<C: Component> TextEdit<C> {
@@ -22,14 +30,22 @@ impl<C: Component> TextEdit<C> {
             .set_width_policy(SizePolicy::expanding(1))
             .set_height_policy(SizePolicy::fixed(minheight));
 
+        let mut cursor_node = uih.new_layout_node();
+        cursor_node
+            .set_width_policy(SizePolicy::fixed(2))
+            .set_height_policy(SizePolicy::fixed(minheight))
+            .set_halign(HorizontalAlignment::Left);
+
         Self {
-            lnode: LeafLayoutNode::new(lnode),
+            lnode,
+            cursor_node: LeafLayoutNode::new(cursor_node),
             text: TextCache::new_with_font_and_size(
                 uih.theme().ui_font.clone(),
                 uih.theme().ui_font_size,
             ),
-            cursor_pos: None,
+            cursor_pos: CursorPosition::End,
             onchange: None,
+            focused: false
         }
     }
 
@@ -37,20 +53,27 @@ impl<C: Component> TextEdit<C> {
         self.text.update(to);
     }
 
-    /*fn update_hints(&mut self, uih: &UIHandle) {
-        let line = uih.theme().make_line(self.text.as_str());
-        let (rendered, offset) = line.render_line();
-        let sz = rendered.size();
-        *self.rendered.borrow_mut() = Some((rendered, offset));
-        self.lnode.render_needed();
-
-        let wp = self.lnode.width_policy().with_minimum(sz.width as usize);
-        self.lnode.set_width_policy(wp);
-        let hp = self.lnode.height_policy().with_minimum(sz.height as usize);
-        self.lnode.set_height_policy(hp);
+    /*pub fn set_cursor_position(&mut self, to: CursorPosition) {
+        
     }*/
 }
 
+impl<C: Component> LayoutTreeNode<()> for TextEdit<C> {
+    fn child(&self, ndx: usize) -> Option<LayoutNodeAccess<'_>> {
+        if ndx == 0 {
+            Some(self.cursor_node.access())
+        } else {
+            None
+        }
+    }
+    fn child_count(&self) -> usize {
+        1
+    }
+    fn current_node(&self) -> &LayoutNode {
+        &self.lnode
+    }
+}
+
 impl<C: Component> Widget<C> for TextEdit<C> {
     fn poll(
         &mut self,
@@ -67,7 +90,6 @@ impl<C: Component> Widget<C> for TextEdit<C> {
                     Key::Named(NamedKey::Backspace) => {
                         self.text.change(String::pop);
                         self.lnode.render_needed();
-                        // self.update_hints(uih);
                     }
                     Key::Named(NamedKey::Delete) => {
                         self.text.change(String::pop);
@@ -85,19 +107,32 @@ impl<C: Component> Widget<C> for TextEdit<C> {
                 }
             }
         }
-        if istate.mouse_status_in(self.lnode.render_area().unwrap(), MouseButton::Left)
-            == MouseCheckStatus::Click
+        let render_area = self.lnode.render_area().unwrap();
+        if istate
+            .mouse_status_in(render_area, MouseButton::Left)
+            .is_click()
         {
             istate.focus_on(self.lnode.id());
+            self.focused = true;
+            println!("click offset: {:?}", istate.mouse.pos - render_area.min);
+
+            let cbar_offset = self.text.size_hint().width;
+            self.cursor_node.margin_mut().left = cbar_offset;
         }
 
-        // self.update_hints(uih);
+        let was_focused = self.focused;
+        self.focused = istate.is_focused(self.lnode.id());
+
+        if self.focused != was_focused {
+            self.cursor_node.render_needed();
+            self.lnode.render_needed();
+        }
 
         vec![]
     }
 
     fn layout_node(&self) -> LayoutNodeAccess {
-        LayoutNodeAccess::new(&self.lnode)
+        LayoutNodeAccess::new(self)
     }
     fn layout_node_mut(&mut self) -> &mut LayoutNode {
         &mut self.lnode
@@ -109,16 +144,9 @@ impl<C: Component> Widget<C> for TextEdit<C> {
             .fill(ColourChoice::Background)
             .simple_text(&self.text, ColourChoice::Foreground);
 
-        /*if self.lnode.render_check() {
-            let area = self.lnode.render_area().unwrap();
-            target.fill_region(area, theme.background);
-
-            let amap = self.rendered.borrow();
-            let Some((amap, offset)) = amap.as_ref() else {
-                return;
-            };
-
-            target.fill_region_masked(amap, PixelBox::from_size(amap.size()), area.min, theme.foreground, kahlo::colour::BlendMode::SourceAlpha);
-        }*/
+        if self.focused {
+            target.with_node(&self.cursor_node)
+                .fill(ColourChoice::Foreground);
+        }
     }
 }

+ 14 - 2
src/window.rs

@@ -96,6 +96,7 @@ impl<WC: WindowComponent> WindowState<WC> {
             PixelBox::from_size(bitmap.size()),
             PixelPoint::zero(),
         );
+
         // let after = std::time::Instant::now();
 
         /*
@@ -205,8 +206,19 @@ impl<WC: WindowComponent> WindowStateAccess for RefCell<WindowState<WC>> {
         is.set_text_input(what.into());
     }
     fn push_keypress(&self, key: winit::keyboard::Key) {
-        let is = &mut self.borrow_mut().istate;
-        is.keypress = Some(key);
+        let mut win = self.borrow_mut();
+        let is = &mut win.istate;
+
+        // special handling for the F12 key, which dumps the current layout tree
+        if key == winit::keyboard::Key::Named(winit::keyboard::NamedKey::F12) {
+            let layout = WindowLayoutHelper::new(&win.root_node, win.wc.root_widget().layout_node());
+
+            let mut tree_file = std::io::BufWriter::new(std::fs::File::create("patina-tree.json").unwrap());
+
+            crate::layout::dump_tree_json(LayoutNodeAccess::new(&layout), &mut tree_file).unwrap();
+        } else {
+            is.keypress = Some(key);
+        }
     }
 }