crabshell-ui 0.2.0

Widgets, styles, layout, text and text editing, focus, hit testing and invalidation for Crabshell
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
//! Layout: taffy computes flexbox and grid layout over the window's nodes.
//!
//! `Row` and `Column` are flex containers. `Stack` and the window itself are
//! one-cell grids, so every child fills the cell. `Button` is a flex
//! container that centers its child. `Text` and `Icon` report their size
//! through taffy's measure function.
//!
//! A layout runs in three passes. The first walks the tree from the root,
//! hands each node's taffy style to taffy and reshapes text whose content or
//! inherited font changed. The second is taffy's. The third walks the tree
//! again to turn taffy's results into window coordinates, lays text out in
//! its final width, and records as damage every node that moved, appeared
//! or changed.

use cosmic_text::FontSystem;
use taffy::prelude::{TaffyGridLine, TaffyGridSpan};
use taffy::{
  AlignItems, AvailableSpace, Dimension, Display, FlexDirection, GridPlacement, JustifyContent,
  LengthPercentage, Line, Overflow,
};

use crate::geometry::{Point, Rect};
use crate::style::Font;
use crate::text::TextSystem;
use crate::text_input::DEFAULT_WIDTH;
use crate::tree::{Node, NodeId, NodeKind, Tree, add_damage, centered, icon_rect};
use crate::widgets::{Align, Justify};

/// The kind of container a node sits in, which decides how it is placed.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Parent {
  /// A flex container, or nothing for the root.
  Flex,
  /// A `Stack`'s one-cell grid.
  Stack,
  /// The window's one-cell grid.
  Window,
  /// A scroll view, whose child keeps its full height.
  Scroll,
}

impl Tree {
  /// Lays the window out at `self.size` and records damage. A popup first
  /// takes the size its content wants, rounded up to whole pixels.
  pub(crate) fn layout(&mut self, text: &mut TextSystem) {
    let root = self.root;
    self.sync(root, Parent::Flex, Font::DEFAULT, text);

    let fonts = text.font_system();
    if self.popup.is_some() {
      self.size = self.natural_size(fonts);
      let mut style = self.taffy.style(root).cloned().unwrap_or_default();
      style.size = definite(self.size);
      let _ = self.taffy.set_style(root, style);
    }
    let available = taffy::Size {
      width: AvailableSpace::Definite(self.size.width),
      height: AvailableSpace::Definite(self.size.height),
    };
    let measured =
      self
        .taffy
        .compute_layout_with_measure(root, available, |inputs, _id, node, style| {
          taffy::compute_leaf_layout(
            inputs,
            style,
            |_, _| 0.0,
            |known, available| measure(node, fonts, known, available),
          )
        });
    if let Err(error) = measured {
      tracing::error!(%error, "taffy failed to lay out a window");
      return;
    }

    self.place(root, Point::default(), fonts);
    self.needs_layout = false;
    self.pointer_after_layout();
  }

  /// The size the window's content wants, in whole logical pixels and at
  /// least 1 a side: its root laid out with no size of its own and
  /// unlimited room.
  fn natural_size(&mut self, fonts: &mut FontSystem) -> crate::Size {
    let root = self.root;
    let mut style = self.taffy.style(root).cloned().unwrap_or_default();
    style.size = taffy::Size::auto();
    let _ = self.taffy.set_style(root, style);
    let unlimited = taffy::Size {
      width: AvailableSpace::MaxContent,
      height: AvailableSpace::MaxContent,
    };
    let measured =
      self
        .taffy
        .compute_layout_with_measure(root, unlimited, |inputs, _id, node, style| {
          taffy::compute_leaf_layout(
            inputs,
            style,
            |_, _| 0.0,
            |known, available| measure(node, fonts, known, available),
          )
        });
    match (measured, self.taffy.layout(root)) {
      // At least 1 px a side: xdg_positioner refuses an empty size.
      (Ok(()), Ok(layout)) => crate::Size::new(
        layout.size.width.ceil().max(1.0),
        layout.size.height.ceil().max(1.0),
      ),
      (Err(error), _) => {
        tracing::error!(%error, "taffy failed to measure a popup");
        self.size
      }
      (_, Err(error)) => {
        tracing::error!(%error, "taffy has no layout for a popup's root");
        self.size
      }
    }
  }

  /// The first pass: gives taffy each node's style, and reshapes text.
  /// `font` is the font inherited from the ancestors.
  fn sync(&mut self, id: NodeId, parent: Parent, font: Font, text: &mut TextSystem) {
    let size = self.size;
    let Some(node) = self.taffy.get_node_context_mut(id) else {
      return;
    };
    let font = node.effective_style().font.unwrap_or(font);
    let mut reshaped = false;
    match &mut node.kind {
      NodeKind::Text(text_node) => reshaped = text_node.prepare(font, text),
      NodeKind::Input(input) => reshaped = input.text.prepare(font, text),
      _ => {}
    }
    let changed = node.changed;
    let style = taffy_style(node, parent, size);
    let child_parent = match node.kind {
      NodeKind::Root => Parent::Window,
      NodeKind::Stack => Parent::Stack,
      NodeKind::Scroll(_) => Parent::Scroll,
      _ => Parent::Flex,
    };

    let same_style = self.taffy.style(id).is_ok_and(|current| *current == style);
    if !same_style {
      // Setting a style also marks the node for measuring again.
      let _ = self.taffy.set_style(id, style);
    } else if reshaped || changed {
      let _ = self.taffy.mark_dirty(id);
    }
    for child in self.taffy.children(id).unwrap_or_default() {
      self.sync(child, child_parent, font, text);
    }
  }

  /// The third pass: turns taffy's layout of `id`, relative to its
  /// parent's corner `origin`, into window coordinates, and records damage.
  /// Returns the bounds of `id` and its descendants.
  fn place(&mut self, id: NodeId, origin: Point, fonts: &mut FontSystem) -> Rect {
    let Ok(&layout) = self.taffy.layout(id) else {
      return Rect::default();
    };
    let rect = Rect::new(
      origin.x + layout.location.x,
      origin.y + layout.location.y,
      layout.size.width,
      layout.size.height,
    );
    let inset_left = layout.padding.left + layout.border.left;
    let inset_top = layout.padding.top + layout.border.top;
    let content = Rect::new(
      rect.x + inset_left,
      rect.y + inset_top,
      (layout.content_box_width()).max(0.0),
      (layout.content_box_height()).max(0.0),
    );

    // A scroll view shows its child moved up by the offset, clamped so
    // neither end scrolls past the box.
    let mut origin = Point::new(rect.x, rect.y);
    if let Some(NodeKind::Scroll(scroll)) = self.node(id).map(|node| &node.kind) {
      let bottom = self
        .taffy
        .children(id)
        .unwrap_or_default()
        .into_iter()
        .filter_map(|child| self.taffy.layout(child).ok())
        .map(|child| {
          child.location.y + child.size.height + layout.padding.bottom + layout.border.bottom
        })
        .fold(0.0, f32::max);
      let most = (bottom - layout.size.height).max(0.0);
      let shown = if scroll.offset.is_nan() {
        0.0
      } else {
        scroll.offset.clamp(0.0, most)
      };
      origin.y -= shown;
      if let Some(NodeKind::Scroll(scroll)) = self.node_mut(id).map(|node| &mut node.kind) {
        scroll.shown = shown;
        scroll.most = most;
      }
    }
    let mut children = Rect::default();
    for child in self.taffy.children(id).unwrap_or_default() {
      children = children.union(self.place(child, origin, fonts));
    }

    let Some(node) = self.taffy.get_node_context_mut(id) else {
      return rect.union(children);
    };
    // What the node itself draws: its rectangle, and text or an icon
    // that overflows it. Only a clipping style cuts its children's
    // overflow; otherwise it shows, as in CSS.
    let mut painted = rect;
    // A fading shadow is drawn anywhere between its start and its end.
    let fading = node.fade.as_ref().map(|fade| fade.from);
    for style in [Some(node.effective_style()), fading].into_iter().flatten() {
      if let Some(shadow) = style.shadow {
        painted = painted.union(shadow.reach(rect));
      }
    }
    match &mut node.kind {
      NodeKind::Text(text) => {
        text.finish(content, fonts);
        painted = painted.union(text.ink_box());
      }
      // A field's text is cut to its content box, so it paints only its
      // own rectangle.
      NodeKind::Input(input) => {
        input.text.finish(content, fonts);
        input.place(content);
      }
      NodeKind::Icon(icon) => painted = painted.union(icon_rect(content, icon.size)),
      NodeKind::Image(image) => {
        painted = painted.union(centered(content, image.width, image.height));
      }
      _ => {}
    }
    // Repaint what the node drew where it was and draws where it is
    // now. A node that draws nothing itself, like a spacer, adds no
    // damage; its children report their own.
    let drawn = if node.draws() {
      painted
    } else {
      Rect::default()
    };
    match node.rect {
      None => add_damage(&mut self.damage, drawn),
      Some(_) if node.painted != painted || node.changed => {
        add_damage(&mut self.damage, node.drawn);
        add_damage(&mut self.damage, drawn);
      }
      Some(_) => {}
    }
    node.drawn = drawn;
    let clips = node.clips();
    let children = if clips {
      children.intersect(rect)
    } else {
      children
    };
    let bounds = painted.union(children);
    // A clip that came, went or changed shape shows or hides parts of
    // children that themselves didn't change.
    if (clips || node.clips) && node.rect.is_some() && (node.changed || node.bounds != bounds) {
      add_damage(&mut self.damage, node.bounds);
      add_damage(&mut self.damage, bounds);
    }
    node.clips = clips;
    node.rect = Some(rect);
    node.content = content;
    node.painted = painted;
    node.bounds = bounds;
    node.changed = false;
    bounds
  }
}

/// Measures a leaf: text through its buffer, an icon as a square, an
/// image at its size.
fn measure(
  node: Option<&mut Node>,
  fonts: &mut FontSystem,
  known: taffy::Size<Option<f32>>,
  available: taffy::Size<AvailableSpace>,
) -> taffy::Size<f32> {
  match node.map(|node| &mut node.kind) {
    Some(NodeKind::Text(text)) => text.measure(fonts, known, available),
    // A field is as tall as a line and doesn't grow as it is typed in.
    Some(NodeKind::Input(input)) => {
      let line = input.text.measure(fonts, known, available);
      taffy::Size {
        width: known.width.unwrap_or(DEFAULT_WIDTH),
        height: line.height,
      }
    }
    Some(NodeKind::Image(image)) => taffy::Size {
      width: known.width.unwrap_or(image.width as f32),
      height: known.height.unwrap_or(image.height as f32),
    },
    Some(NodeKind::Icon(icon)) => {
      let side = icon.size as f32;
      taffy::Size {
        width: known.width.unwrap_or(side),
        height: known.height.unwrap_or(side),
      }
    }
    _ => taffy::Size {
      width: known.width.unwrap_or(0.0),
      height: known.height.unwrap_or(0.0),
    },
  }
}

/// The taffy style for `node`, which sits in a `parent` container. `size`
/// is the window's, which the root takes.
fn taffy_style(node: &Node, parent: Parent, size: crate::Size) -> taffy::Style {
  let padding = node.padding();
  let vertical = LengthPercentage::length(padding.vertical as f32);
  let horizontal = LengthPercentage::length(padding.horizontal as f32);
  let mut style = taffy::Style {
    padding: taffy::Rect {
      left: horizontal,
      right: horizontal,
      top: vertical,
      bottom: vertical,
    },
    ..taffy::Style::default()
  };
  match &node.kind {
    NodeKind::Root => {
      style.display = Display::Grid;
      style.size = definite(size);
      style.grid_template_columns = vec![taffy::prelude::fr(1.0)];
      style.grid_template_rows = vec![taffy::prelude::fr(1.0)];
    }
    NodeKind::Flex(flex) => {
      style.flex_direction = if flex.column {
        FlexDirection::Column
      } else {
        FlexDirection::Row
      };
      if flex.grow {
        style.flex_grow = 1.0;
      }
      let gap = LengthPercentage::length(flex.gap as f32);
      style.gap = taffy::Size {
        width: gap,
        height: gap,
      };
      style.align_items = match flex.align {
        Align::Start => AlignItems::START,
        Align::Center => AlignItems::CENTER,
        Align::End => AlignItems::END,
        Align::Stretch => AlignItems::STRETCH,
      };
      style.justify_content = match flex.justify {
        Justify::Start => JustifyContent::START,
        Justify::Center => JustifyContent::CENTER,
        Justify::End => JustifyContent::END,
        Justify::SpaceBetween => JustifyContent::SPACE_BETWEEN,
        Justify::SpaceAround => JustifyContent::SPACE_AROUND,
        Justify::SpaceEvenly => JustifyContent::SPACE_EVENLY,
      };
    }
    NodeKind::Stack => style.display = Display::Grid,
    NodeKind::Scroll(_) => {
      style.flex_direction = FlexDirection::Column;
      // As in CSS, a scroll container isn't held to its content's size.
      style.overflow = taffy::Point {
        x: Overflow::Visible,
        y: Overflow::Scroll,
      };
      style.scrollbar_width = 0.0;
    }
    NodeKind::Spacer => style.flex_grow = 1.0,
    NodeKind::Button(_) => {
      style.align_items = AlignItems::CENTER;
      style.justify_content = JustifyContent::CENTER;
    }
    NodeKind::Text(_)
    | NodeKind::Icon(_)
    | NodeKind::Image(_)
    | NodeKind::Capture(_)
    | NodeKind::Input(_) => {}
  }
  // A size the style sets wins over the content's, but never over the
  // window's own.
  let own = node.effective_style();
  if !matches!(node.kind, NodeKind::Root) {
    let length = |value: Option<f32>| match value {
      Some(value) if value.is_finite() => Dimension::length(value.max(0.0)),
      _ => Dimension::auto(),
    };
    style.size = taffy::Size {
      width: length(own.width),
      height: length(own.height),
    };
  }
  if parent == Parent::Scroll {
    // A child with a height of its own would otherwise shrink to what its
    // content needs.
    style.flex_shrink = 0.0;
  }
  if matches!(parent, Parent::Stack | Parent::Window) {
    let cell = Line {
      start: GridPlacement::from_line_index(1),
      end: GridPlacement::from_span(1),
    };
    style.grid_row = cell.clone();
    style.grid_column = cell;
  }
  if parent == Parent::Window {
    // As in CSS, an item can't be smaller than its content by default.
    // The window's size comes from the compositor, so its child is
    // exactly that size, and content too wide overflows inside it.
    style.min_size = taffy::Size {
      width: taffy::LengthPercentageAuto::length(0.0),
      height: taffy::LengthPercentageAuto::length(0.0),
    };
  }
  style
}

/// A taffy size of exactly `size`.
fn definite(size: crate::Size) -> taffy::Size<taffy::Dimension> {
  taffy::Size {
    width: taffy::Dimension::length(size.width),
    height: taffy::Dimension::length(size.height),
  }
}