telar_ui_core/
line_gutter.rs1use std::rc::Rc;
2
3use geometry_core::Rect;
4use layout_core::{LayoutError, LayoutStyle};
5use platform_core::Event;
6use reactive_core::{Effect, effect};
7use renderer_core::TextStyle;
8use ui_tree::{Component, EventResult, RenderNode};
9
10use crate::context::{mark_dirty, new_measured_leaf};
11use crate::impl_leaf_widget;
12use crate::layout_leaf::LayoutLeaf;
13
14const NO_WRAP_WIDTH: f32 = 1.0e6;
16
17pub struct LineGutter {
24 line_count: Rc<dyn Fn() -> usize>,
25 style: Rc<dyn Fn() -> TextStyle>,
26 leaf: LayoutLeaf,
27 _remeasure: Effect,
30}
31
32impl LineGutter {
33 pub fn new(
34 line_count: impl Fn() -> usize + 'static,
35 layout_style: LayoutStyle,
36 style_fn: impl Fn() -> TextStyle + 'static,
37 ) -> Result<Self, LayoutError> {
38 let line_count: Rc<dyn Fn() -> usize> = Rc::new(line_count);
39 let style: Rc<dyn Fn() -> TextStyle> = Rc::new(style_fn);
40
41 let measure_count = Rc::clone(&line_count);
42 let measure_style = Rc::clone(&style);
43 let measure = Box::new(move |_max_width: f32| {
44 let s = (measure_style)();
45 let line_h = s.font_size * renderer_text::LINE_HEIGHT_FACTOR;
46 let n = (measure_count)().max(1);
47 let width = renderer_text::measure_text(&n.to_string(), NO_WRAP_WIDTH, &s).0;
49 (width, n as f32 * line_h)
50 });
51 let (node, rect) = new_measured_leaf(layout_style, measure)?;
52 let remeasure = {
53 let line_count = Rc::clone(&line_count);
54 effect(move || {
55 let _ = (line_count)();
57 mark_dirty(node).ok();
58 })
59 };
60 Ok(Self {
61 line_count,
62 style,
63 leaf: LayoutLeaf { node, rect },
64 _remeasure: remeasure,
65 })
66 }
67}
68
69impl Component for LineGutter {
70 fn view(&self) -> RenderNode {
71 let style = (self.style)();
72 let line_h = style.font_size * renderer_text::LINE_HEIGHT_FACTOR;
73 let n = (self.line_count)().max(1);
74 let mut numbers = String::new();
75 for i in 1..=n {
76 if i > 1 {
77 numbers.push('\n');
78 }
79 numbers.push_str(&i.to_string());
80 }
81 let r = self.leaf.rect.get();
82 let full = Rect {
85 x: 0.0,
86 y: 0.0,
87 width: r.width.max(1.0),
88 height: (n as f32 * line_h).max(line_h),
89 };
90 self.leaf
91 .at_layout_position(RenderNode::text(numbers, full, style))
92 }
93
94 fn on_event(&mut self, _event: &Event) -> EventResult {
95 EventResult::Ignored
96 }
97
98 fn debug_name(&self) -> &'static str {
99 "LineGutter"
100 }
101}
102
103impl_leaf_widget!(LineGutter);
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::context::{compute_layout, new_container, reset_layout_runtime};
109 use layout_core::AvailableSpace;
110 use reactive_core::{RwSignal, signal};
111 use renderer_core::Color;
112
113 fn gutter(count: RwSignal<usize>) -> LineGutter {
114 LineGutter::new(
115 move || count.get(),
116 LayoutStyle::new(),
117 || TextStyle::new(14.0, Color::BLACK),
118 )
119 .unwrap()
120 }
121
122 #[test]
125 fn height_tracks_line_count() {
126 reset_layout_runtime();
127 let count = signal(3usize);
128 let g = gutter(count.clone());
129 let rect = g.leaf.rect;
130 let root = new_container(
131 LayoutStyle::new().flex_column().width(200.0),
132 &[g.leaf.node],
133 )
134 .unwrap();
135 let line_h = 14.0 * renderer_text::LINE_HEIGHT_FACTOR;
136 compute_layout(
137 root,
138 AvailableSpace::Definite(200.0),
139 AvailableSpace::MaxContent,
140 )
141 .unwrap();
142 assert!(
143 (rect.get().height - 3.0 * line_h).abs() < 0.5,
144 "3 lines: {:?}",
145 rect.get()
146 );
147
148 count.set(10);
149 compute_layout(
150 root,
151 AvailableSpace::Definite(200.0),
152 AvailableSpace::MaxContent,
153 )
154 .unwrap();
155 assert!(
156 (rect.get().height - 10.0 * line_h).abs() < 0.5,
157 "grew to 10 lines: {:?}",
158 rect.get()
159 );
160 assert!(
162 rect.get().width > 0.0,
163 "gutter reserves a width: {:?}",
164 rect.get()
165 );
166 }
167}