1use crate::element::{Align, BoxStyle, Element, ElementKind};
15use crate::runtime::{
16 ActionId, FocusId, LayoutNode, NodeContent, ScrollId, find_action, find_focus, first_text,
17};
18use stipple_geometry::{Point, Rect, Size, Vec2};
19use stipple_layout::{Axis, FlexItem, solve_main_axis};
20use stipple_render::{Color, Font, Scene};
21
22const VIEWPORT_PLACEHOLDER: Color = Color::rgb(0x20, 0x24, 0x2c);
26
27pub fn measure(el: &Element, avail: Size, font: Option<&Font>) -> Size {
29 let pad = el.layout.padding;
30 let inner = avail.deflate(pad);
31
32 let content = match &el.kind {
33 ElementKind::Leaf | ElementKind::Viewport { .. } => Size::ZERO,
36 ElementKind::Text { text, size, .. } => match font {
37 Some(f) if el.wrap && inner.width.is_finite() => {
39 let lines = f.wrap(text, *size, inner.width);
40 let w = lines
41 .iter()
42 .map(|l| f.measure(l, *size).width)
43 .fold(0.0_f64, f64::max);
44 Size::new(w, f.line_height(*size) * lines.len() as f64)
45 }
46 Some(f) => f.measure(text, *size),
47 None => Size::ZERO,
48 },
49 ElementKind::Stack {
50 axis,
51 gap,
52 children,
53 ..
54 } => {
55 let mut main = 0.0;
56 let mut cross: f64 = 0.0;
57 for c in children {
58 let cs = measure(c, inner, font);
59 main += axis.main(cs);
60 cross = cross.max(axis.cross(cs));
61 }
62 if children.len() > 1 {
63 main += gap * (children.len() as f64 - 1.0);
64 }
65 axis.size(main, cross)
66 }
67 };
68
69 let w = el
70 .layout
71 .size
72 .width
73 .unwrap_or(content.width + pad.horizontal());
74 let h = el
75 .layout
76 .size
77 .height
78 .unwrap_or(content.height + pad.vertical());
79 Size::new(w, h)
80}
81
82pub fn layout(el: &Element, bounds: Rect, font: Option<&Font>) -> LayoutNode {
85 let content = match &el.kind {
86 ElementKind::Text { text, size, color } => NodeContent::Text {
87 text: text.clone(),
88 size: *size,
89 color: *color,
90 },
91 ElementKind::Viewport { id } => NodeContent::Viewport(*id),
92 _ => NodeContent::None,
93 };
94 let mut node = LayoutNode {
95 bounds,
96 decoration: el.decoration,
97 content,
98 action: el.action,
99 focus: el.focus,
100 drag: el.drag,
101 context: el.context,
102 caret: el.caret,
103 selection: el.selection,
104 text_pos: el.text_pos,
105 wrap: el.wrap,
106 scroll: el.scroll,
107 clip: el.clip,
108 children: Vec::new(),
109 };
110
111 let ElementKind::Stack {
112 axis,
113 gap,
114 main_align,
115 cross_align,
116 children,
117 } = &el.kind
118 else {
119 return node;
120 };
121 if children.is_empty() {
122 return node;
123 }
124
125 let inner = bounds.inset(el.layout.padding);
126 let avail = inner.size;
127 let axis = *axis;
128
129 let measured: Vec<Size> = children.iter().map(|c| measure(c, avail, font)).collect();
131 let (spans, main_shift) = if el.scroll.is_some() {
135 let mut spans = Vec::with_capacity(children.len());
136 let mut cursor = 0.0;
137 for m in &measured {
138 let length = axis.main(*m);
139 spans.push(stipple_layout::Span {
140 offset: cursor,
141 length,
142 });
143 cursor += length + *gap;
144 }
145 (spans, 0.0)
146 } else {
147 let items: Vec<FlexItem> = children
148 .iter()
149 .zip(&measured)
150 .map(|(c, m)| FlexItem {
151 basis: axis.main(*m),
152 grow: c.layout.grow,
153 })
154 .collect();
155 let spans = solve_main_axis(axis.main(avail), *gap, &items);
156 let used_main = spans.last().map(|s| s.offset + s.length).unwrap_or(0.0);
159 let leftover = (axis.main(avail) - used_main).max(0.0);
160 let main_shift = match main_align {
161 Align::Start | Align::Stretch => 0.0,
162 Align::Center => leftover / 2.0,
163 Align::End => leftover,
164 };
165 (spans, main_shift)
166 };
167
168 node.children.reserve(children.len());
169 for ((child, m), span) in children.iter().zip(&measured).zip(&spans) {
170 let cross_avail = axis.cross(avail);
171 let cross_len = match cross_align {
172 Align::Stretch => cross_avail,
173 _ => axis.cross(*m).min(cross_avail),
174 };
175 let cross_off = match cross_align {
176 Align::Start | Align::Stretch => 0.0,
177 Align::Center => (cross_avail - cross_len) / 2.0,
178 Align::End => cross_avail - cross_len,
179 };
180 let child_bounds = child_rect(
181 axis,
182 inner,
183 span.offset + main_shift,
184 span.length,
185 cross_off,
186 cross_len,
187 );
188 node.children.push(layout(child, child_bounds, font));
189 }
190 node
191}
192
193pub fn paint_focus(
198 tree: &LayoutNode,
199 focused: FocusId,
200 scene: &mut Scene,
201 font: Option<&Font>,
202 ring: Color,
203 caret: Color,
204 selection: Color,
205) {
206 let Some(node) = find_focus(tree, focused) else {
207 return;
208 };
209 scene.stroke_rect(node.bounds, ring, 2.0);
210
211 let Some(leaf) = first_text(node) else { return };
212 let NodeContent::Text { text, size, .. } = &leaf.content else {
213 return;
214 };
215 let bounds = leaf.bounds;
216 let size = *size;
217 let line_h = font.map(|f| f.line_height(size)).unwrap_or(size);
218 let width = |slice: &str| font.map(|f| f.measure(slice, size).width).unwrap_or(0.0);
220 let pos = |i: usize| -> (f64, usize) {
222 let i = i.min(text.len());
223 let ls = text[..i].rfind('\n').map(|n| n + 1).unwrap_or(0);
224 let line = text[..ls].bytes().filter(|&b| b == b'\n').count();
225 (bounds.min_x() + width(&text[ls..i]), line)
226 };
227
228 if let Some((s, e)) = leaf.selection
231 && e > s
232 {
233 let mut ls = 0usize;
234 let mut line = 0usize;
235 loop {
236 let le = text[ls..].find('\n').map(|n| ls + n).unwrap_or(text.len());
237 let nl = if le < text.len() { 1 } else { 0 };
240 let sel_s = s.max(ls);
241 let sel_e = e.min(le + nl);
242 if sel_e > sel_s && sel_s <= le {
243 let x0 = bounds.min_x() + width(&text[ls..sel_s.min(le)]);
244 let x1 = bounds.min_x() + width(&text[ls..sel_e.min(le)]);
245 let extra = if sel_e > le { 6.0 } else { 0.0 }; let y = bounds.min_y() + line as f64 * line_h;
247 scene.fill_rect(Rect::from_xywh(x0, y, (x1 - x0) + extra, line_h), selection);
248 }
249 if le >= text.len() {
250 break;
251 }
252 ls = le + 1;
253 line += 1;
254 }
255 }
256
257 let (cx, cline) = pos(leaf.caret.unwrap_or(text.len()));
259 let cx = (cx + 1.0).min(bounds.max_x().max(bounds.min_x() + 1.0));
260 let cy = bounds.min_y() + cline as f64 * line_h;
261 scene.fill_rect(Rect::from_xywh(cx, cy, 2.0, line_h), caret);
262}
263
264pub fn caret_index_at(node: &LayoutNode, point: Point, font: Option<&Font>) -> Option<usize> {
269 let leaf = first_text(node)?;
270 let NodeContent::Text { text, size, .. } = &leaf.content else {
271 return None;
272 };
273 let font = font?;
274 let line_h = font.line_height(*size).max(1.0);
275 let line = ((point.y - leaf.bounds.min_y()) / line_h).floor().max(0.0) as usize;
276
277 let (mut ls, mut le) = (0usize, text.len());
280 let mut start = 0usize;
281 for (i, part) in text.split('\n').enumerate() {
282 let end = start + part.len();
283 ls = start;
284 le = end;
285 if i == line {
286 break;
287 }
288 start = end + 1; }
290
291 let local = point.x - leaf.bounds.min_x();
292 if local <= 0.0 {
293 return Some(ls);
294 }
295 let line_text = &text[ls..le];
297 let mut best = ls;
298 let mut best_dist = f64::INFINITY;
299 for (off, _) in line_text
300 .char_indices()
301 .map(|(i, _)| (i, ()))
302 .chain(std::iter::once((line_text.len(), ())))
303 {
304 let w = font.measure(&line_text[..off], *size).width;
305 let d = (w - local).abs();
306 if d < best_dist {
307 best_dist = d;
308 best = ls + off;
309 }
310 }
311 Some(best)
312}
313
314pub fn paint_hover(tree: &LayoutNode, hovered: ActionId, scene: &mut Scene, highlight: Color) {
317 if let Some(node) = find_action(tree, hovered) {
318 if node.decoration.radius > 0.0 {
319 scene.fill_round_rect(node.bounds, node.decoration.radius, highlight);
320 } else {
321 scene.fill_rect(node.bounds, highlight);
322 }
323 }
324}
325
326pub fn paint(node: &LayoutNode, scene: &mut Scene, font: Option<&Font>) {
328 paint_decoration(&node.decoration, node.bounds, scene);
329 match &node.content {
330 NodeContent::Text { text, size, color } => {
331 if let Some(f) = font {
332 if node.wrap {
333 let wrapped = f.wrap(text, *size, node.bounds.width()).join("\n");
335 scene.fill_text(f, &wrapped, node.bounds.origin, *size, *color);
336 } else {
337 scene.fill_text(f, text, node.bounds.origin, *size, *color);
338 }
339 }
340 }
341 NodeContent::Viewport(id) => {
344 scene.fill_viewport(node.bounds, id.0 as u64, VIEWPORT_PLACEHOLDER);
345 }
346 NodeContent::None => {}
347 }
348 if node.clip && !node.children.is_empty() {
351 scene.push_clip(node.bounds);
352 for child in &node.children {
353 paint(child, scene, font);
354 }
355 scene.pop_clip();
356 } else {
357 for child in &node.children {
358 paint(child, scene, font);
359 }
360 }
361}
362
363pub fn apply_scroll(node: &mut LayoutNode, offsets: &mut std::collections::HashMap<ScrollId, f64>) {
368 if let Some(id) = node.scroll {
369 let top = node.bounds.min_y();
372 let content_bottom = node
373 .children
374 .iter()
375 .map(|c| c.bounds.max_y())
376 .fold(top, f64::max);
377 let content_h = content_bottom - top;
378 let max_off = (content_h - node.bounds.height()).max(0.0);
379 let off = offsets.entry(id).or_insert(0.0);
380 *off = off.clamp(0.0, max_off);
381 let dy = *off;
382 if dy > 0.0 {
383 for child in &mut node.children {
384 translate(child, -dy);
385 }
386 }
387 }
388 for child in &mut node.children {
389 apply_scroll(child, offsets);
390 }
391}
392
393fn translate(node: &mut LayoutNode, dy: f64) {
395 node.bounds = node.bounds.translate(Vec2::new(0.0, dy));
396 for child in &mut node.children {
397 translate(child, dy);
398 }
399}
400
401fn child_rect(
402 axis: Axis,
403 content: Rect,
404 main_off: f64,
405 main_len: f64,
406 cross_off: f64,
407 cross_len: f64,
408) -> Rect {
409 match axis {
410 Axis::Horizontal => Rect::from_xywh(
411 content.min_x() + main_off,
412 content.min_y() + cross_off,
413 main_len,
414 cross_len,
415 ),
416 Axis::Vertical => Rect::from_xywh(
417 content.min_x() + cross_off,
418 content.min_y() + main_off,
419 cross_len,
420 main_len,
421 ),
422 }
423}
424
425fn paint_decoration(deco: &BoxStyle, bounds: Rect, scene: &mut Scene) {
426 if let Some(fill) = deco.fill {
427 if deco.radius > 0.0 {
428 scene.fill_round_rect(bounds, deco.radius, fill);
429 } else {
430 scene.fill_rect(bounds, fill);
431 }
432 }
433 if let Some((color, width)) = deco.border {
434 scene.stroke_rect(bounds, color, width);
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::element::BoxStyle;
442 use stipple_render::Color;
443
444 #[test]
445 fn measure_sums_children_with_gap_and_padding() {
446 let child = || Element::boxed(BoxStyle::default()).width(20.0).height(10.0);
447 let stack = Element::stack(Axis::Vertical, vec![child(), child(), child()])
448 .gap(5.0)
449 .padding(stipple_geometry::Insets::uniform(4.0));
450 let size = measure(&stack, Size::new(1000.0, 1000.0), None);
452 assert_eq!(size, Size::new(28.0, 48.0));
453 }
454
455 #[test]
456 fn caret_index_at_resolves_pointer_to_byte_index() {
457 let Some(font) = Font::system_default() else {
458 eprintln!("skipping: no system font found");
459 return;
460 };
461 let el = Element::text("hello", 16.0, Color::BLACK);
463 let node = layout(&el, Rect::from_xywh(10.0, 0.0, 200.0, 20.0), Some(&font));
464 let y = node.bounds.min_y() + 1.0;
465 assert_eq!(
467 caret_index_at(&node, Point::new(0.0, y), Some(&font)),
468 Some(0)
469 );
470 assert_eq!(
471 caret_index_at(&node, Point::new(9.0, y), Some(&font)),
472 Some(0)
473 );
474 assert_eq!(
475 caret_index_at(&node, Point::new(10_000.0, y), Some(&font)),
476 Some(5)
477 );
478 let mid = node.bounds.min_x() + font.measure("hel", 16.0).width;
480 let i = caret_index_at(&node, Point::new(mid, y), Some(&font)).unwrap();
481 assert!((1..=4).contains(&i), "mid index {i} out of range");
482 assert_eq!(caret_index_at(&node, Point::new(mid, y), None), None);
484 }
485
486 #[test]
487 fn viewport_reserves_its_rect_and_paints_a_viewport_command() {
488 use crate::runtime::{ViewportId, collect_viewports, viewport_at};
489 use stipple_render::DrawCmd;
490
491 let el = Element::viewport(ViewportId(3)).width(120.0).height(80.0);
492 assert_eq!(
494 measure(&el, Size::new(1000.0, 1000.0), None),
495 Size::new(120.0, 80.0)
496 );
497
498 let tree = layout(&el, Rect::from_xywh(20.0, 10.0, 120.0, 80.0), None);
499 assert!(matches!(tree.content, NodeContent::Viewport(ViewportId(3))));
500
501 let mut found = Vec::new();
503 collect_viewports(&tree, &mut found);
504 assert_eq!(
505 found,
506 vec![(ViewportId(3), Rect::from_xywh(20.0, 10.0, 120.0, 80.0))]
507 );
508 assert_eq!(
509 viewport_at(&tree, Point::new(30.0, 20.0)).map(|(id, _)| id),
510 Some(ViewportId(3))
511 );
512 assert_eq!(viewport_at(&tree, Point::new(0.0, 0.0)), None);
513
514 let mut scene = Scene::new(Size::new(200.0, 120.0));
516 paint(&tree, &mut scene, None);
517 assert!(
518 scene
519 .commands()
520 .iter()
521 .any(|c| matches!(c, DrawCmd::Viewport { id: 3, .. }))
522 );
523 }
524
525 #[test]
526 fn grow_child_fills_main_axis() {
527 let fixed = Element::boxed(BoxStyle::default()).width(40.0).height(10.0);
529 let flex = Element::boxed(BoxStyle {
530 fill: Some(Color::WHITE),
531 ..BoxStyle::default()
532 })
533 .grow(1.0);
534 let row =
535 Element::stack(Axis::Horizontal, vec![fixed, flex]).align(Align::Start, Align::Stretch);
536
537 let tree = layout(&row, Rect::from_xywh(0.0, 0.0, 200.0, 20.0), None);
538 assert_eq!(
541 tree.children[1].bounds,
542 Rect::from_xywh(40.0, 0.0, 160.0, 20.0)
543 );
544
545 let mut scene = Scene::new(Size::new(200.0, 20.0));
546 paint(&tree, &mut scene, None);
547 assert_eq!(scene.len(), 1); }
549}