brep_app/panels/tree.rs
1//! A reusable, custom-painted TREE-NODE widget — the shared building block for
2//! the engine-native side panels (the history feature tree here; the Scene tree
3//! and others reuse it verbatim). egui's default `CollapsingHeader` draws a
4//! disclosure TRIANGLE and no connector rules; the design reference is a classic
5//! file-tree with **`[+]`/`[-]` collapse boxes + connector lines**, so this
6//! module paints those itself.
7//!
8//! # The node model (immutable-mode friendly)
9//!
10//! A node is ONE row: `⟨connectors⟩ [+/-] ⟨glyph⟩ ⟨label⟩ … ⟨right content⟩`.
11//! Expansion state is OWNED BY THE CALLER (the panels need exclusive-expand +
12//! roll-to side effects), so [`node`] just draws a row and returns what was
13//! clicked; the caller decides whether to recurse into children.
14//!
15//! ## Connector geometry
16//!
17//! Indentation is one [`INDENT`] column per tree level. A node at connector
18//! column `d = guides.len()` draws its `├`/`└` connector at column `d` and its
19//! box slot at column `d + 1`. `guides[i]` (`i < d`) says whether an ANCESTOR's
20//! sibling line passes vertically through this row at column `i`. Descending into
21//! a node's children extends the guide stack by [`child_guides`] with
22//! `!this_node_is_last` — the node's own connector line continues down past its
23//! children only if it has following siblings. This is the standard file-tree
24//! rule and yields the reference's exact rules.
25//!
26//! ## Select and open
27//!
28//! Every tree reads a row the same way: a single click SELECTS the row's object
29//! ([`NodeResponse::clicked`]) and a double click OPENS its dialog
30//! ([`NodeResponse::double_clicked`]). A selected row is drawn with the
31//! selection band behind its whole width ([`TreeRow::highlighted`]), the look
32//! the column trees give theirs.
33//!
34//! This module has NO engine dependency — it is pure egui + geometry, so a later
35//! `brep-ui` crate (and the theme pass, #49) can reuse it unchanged.
36
37use crate::icon_text::IconText;
38use eframe::egui;
39
40/// One tree indentation level, in egui points.
41pub const INDENT: f32 = 16.0;
42/// The collapse box's side length.
43const BOX: f32 = 13.0;
44/// Gap between the box slot, an optional glyph, and the label.
45const GAP: f32 = 4.0;
46/// Right-side gutter reserved before the right-aligned row content (timing,
47/// delete X, field inputs, a Select button) so those controls clear the sidebar
48/// scroll bar instead of underlapping it — an underlapping X eats the click as a
49/// scroll drag rather than firing.
50const RIGHT_PAD: f32 = 10.0;
51
52/// The spec for one tree row. Borrows its strings; holds no state (expansion is
53/// the caller's — see the module docs).
54pub struct TreeRow<'a> {
55 /// Ancestor vertical-guide flags, one per level strictly above this node
56 /// (`true` = a sibling line passes through this row at that level). The
57 /// node's own connector column index is `guides.len()`.
58 pub guides: &'a [bool],
59 /// This node is the LAST among its siblings (`└` vs `├`). Ignored when `root`.
60 pub is_last: bool,
61 /// Draw a `[+]`/`[-]` collapse box (an expandable node) instead of an empty
62 /// box slot (a leaf). A leaf keeps the same label indent so labels align.
63 pub expandable: bool,
64 /// Current expand state — only meaningful when `expandable`.
65 pub expanded: bool,
66 /// The far-left ROOT row (e.g. `Features`): no connector, box at column 0.
67 pub root: bool,
68 /// An optional per-type glyph drawn between the box slot and the label.
69 pub glyph: Option<&'a str>,
70 /// The row's text label.
71 pub label: &'a str,
72 /// Emphasize the label (the selected node).
73 pub selected: bool,
74 /// Paint the selection band behind the whole row — the selected LINE.
75 /// [`Self::selected`] sets it with the emphasis; a column tree, which paints
76 /// its own band across every column, draws its tree cell without it.
77 pub highlighted: bool,
78 /// Make the label a drag handle (`Sense::click_and_drag`) — drag-reorder.
79 pub draggable: bool,
80 /// Override the label's text color (the sketch panel paints a CONFLICTING
81 /// constraint's row red). `None` keeps the theme's text color, including its
82 /// hover/selection behavior.
83 pub tint: Option<egui::Color32>,
84}
85
86impl<'a> TreeRow<'a> {
87 /// A plain expandable branch node.
88 pub fn branch(guides: &'a [bool], is_last: bool, expanded: bool, label: &'a str) -> Self {
89 Self {
90 guides,
91 is_last,
92 expandable: true,
93 expanded,
94 root: false,
95 glyph: None,
96 label,
97 selected: false,
98 highlighted: false,
99 draggable: false,
100 tint: None,
101 }
102 }
103
104 /// A plain leaf node (no collapse box).
105 pub fn leaf(guides: &'a [bool], is_last: bool, label: &'a str) -> Self {
106 Self {
107 guides,
108 is_last,
109 expandable: false,
110 expanded: false,
111 root: false,
112 glyph: None,
113 label,
114 selected: false,
115 highlighted: false,
116 draggable: false,
117 tint: None,
118 }
119 }
120
121 pub fn glyph(mut self, glyph: Option<&'a str>) -> Self {
122 self.glyph = glyph;
123 self
124 }
125 /// Mark the row selected: its label emphasized and its line highlighted.
126 pub fn selected(mut self, selected: bool) -> Self {
127 self.selected = selected;
128 self.highlighted = selected;
129 self
130 }
131 pub fn draggable(mut self, draggable: bool) -> Self {
132 self.draggable = draggable;
133 self
134 }
135 pub fn tint(mut self, tint: Option<egui::Color32>) -> Self {
136 self.tint = tint;
137 self
138 }
139}
140
141/// What happened to a drawn tree row.
142pub struct NodeResponse {
143 /// The whole row rect (for hit publishing / drag-target hit-testing).
144 pub row_rect: egui::Rect,
145 /// The collapse-box rect (for hit publishing).
146 pub box_rect: egui::Rect,
147 /// The `[+]`/`[-]` collapse box was clicked (a pure toggle intent).
148 pub toggled: bool,
149 /// The label response — the DRAG handle and the hover source. Use
150 /// `.drag_started()` / `.drag_stopped()` to reorder and `.hovered()` for a
151 /// text-keyed highlight. For "the row was clicked" use [`Self::clicked`],
152 /// not this: the label's rect is only its own text.
153 pub label: egui::Response,
154 /// The whole ROW BAND, sensing clicks BELOW everything drawn in the row.
155 /// egui hands an overlapped point to the last widget registered there and
156 /// this one is registered first (`UiBuilder::sense`), so the collapse box,
157 /// the label and every control the caller put in `add_right` still take
158 /// their own clicks — the band only ever receives what none of them wanted,
159 /// which is exactly the blank space between them.
160 ///
161 /// It is also the row's KEYBOARD stop: the label is deliberately not
162 /// focusable, so a row is one Tab stop and Enter/Space on it reports
163 /// `clicked()` here.
164 ///
165 /// It senses DRAG as well as click, and it has to: egui withdraws the click
166 /// from a press that outlives `max_click_duration` (0.8 s) or drifts past
167 /// `max_click_dist` (6 pt) whether or not the widget senses drag, so a
168 /// click-only band would go dead under exactly the slow, wobbly press an
169 /// ordinary hand makes. A caller resolves that press through
170 /// [`Self::drag_started`], as the history and constraint trees do.
171 pub band: egui::Response,
172 /// The BAND's drag started, and its press did not begin on one of the row's
173 /// own controls (the collapse box, anything `add_right` drew). Read through
174 /// [`Self::drag_started`].
175 pub band_drag_started: bool,
176}
177
178impl NodeResponse {
179 /// The ROW was clicked — on its text or anywhere else in its band that no
180 /// control of its own claimed. This, not `label.clicked()`, is what
181 /// "the user picked this row" means: a row is a click target across its
182 /// whole width, and a label's rect is only as wide as its text. A click
183 /// SELECTS the row's object.
184 pub fn clicked(&self) -> bool {
185 self.label.clicked() || self.band.clicked()
186 }
187
188 /// The ROW was double-clicked, over the same area as [`Self::clicked`]. A
189 /// double click OPENS the row's dialog. egui reports the second press as a
190 /// click as well, so on this frame [`Self::clicked`] is true too: a
191 /// consumer checks this first.
192 ///
193 /// egui's TRIPLE click counts. egui numbers a click 3 when the click two
194 /// before it is under 0.6 s old, and a 3 is not a 2 — so a row clicked once
195 /// and then double-clicked a moment later would otherwise never open.
196 pub fn double_clicked(&self) -> bool {
197 is_double_click(&self.label) || is_double_click(&self.band)
198 }
199}
200
201/// A response's double click, egui's triple included — see
202/// [`NodeResponse::double_clicked`]. The column trees read their row bands
203/// through this too.
204pub fn is_double_click(response: &egui::Response) -> bool {
205 response.double_clicked() || response.triple_clicked()
206}
207
208impl NodeResponse {
209
210 /// A press on the row turned into a DRAG — on the label or anywhere else
211 /// along the band. A pane that reorders by drag arms itself from this; a
212 /// pane that does not still wants it, because a press egui reclassified as
213 /// a drag is the click it refused to report, and resolving it is the only
214 /// way a slow click on a row still opens that row.
215 pub fn drag_started(&self) -> bool {
216 self.label.drag_started() || self.band_drag_started
217 }
218}
219
220/// Extend a guide stack for a node's children: the node's own connector column
221/// keeps drawing a vertical line past the children ONLY if the node has more
222/// siblings below it (`!is_last`).
223pub fn child_guides(guides: &[bool], is_last: bool) -> Vec<bool> {
224 let mut next = guides.to_vec();
225 next.push(!is_last);
226 next
227}
228
229/// Draw ONE tree row and return what was interacted with. `add_right` fills the
230/// right-aligned content area (timing + delete, field inputs, a Select button…).
231///
232/// The whole row is a click target: read [`NodeResponse::clicked`], which is the
233/// band and the label together. [`NodeResponse::label`] remains the reorder
234/// drag handle and the source of a text-keyed hover.
235/// The hover band's colour: egui's own hovered-widget fill, faint.
236pub(crate) fn hover_band_fill(visuals: &egui::Visuals) -> egui::Color32 {
237 visuals.widgets.hovered.weak_bg_fill.gamma_multiply(0.5)
238}
239
240pub fn node(
241 ui: &mut egui::Ui,
242 row: TreeRow,
243 add_right: impl FnOnce(&mut egui::Ui),
244) -> NodeResponse {
245 let connector_col = if row.root { 0 } else { row.guides.len() };
246 // The box slot (and thus the label) begins one column right of the connector.
247 let indent = if row.root {
248 0.0
249 } else {
250 (connector_col as f32 + 1.0) * INDENT
251 };
252
253 let mut box_rect = egui::Rect::NOTHING;
254 let mut toggled = false;
255 let mut label_resp: Option<egui::Response> = None;
256 // The selection band's slot, reserved BEFORE the row so the row's contents
257 // paint over it; filled once the row's rect is known.
258 let highlight = ui.painter().add(egui::Shape::Noop);
259 // The row's OWN controls — the collapse box and whatever `add_right` drew.
260 // A press that starts on one of those belongs to it, never to the band.
261 let mut controls = egui::Rect::NOTHING;
262
263 // THE ROW BAND. A row is a click target across its whole width, not only
264 // across its text — so the row is drawn inside a `Ui` that senses clicks
265 // itself. `UiBuilder::sense` registers that rect BEFORE the row's contents
266 // and updates it in place when the scope closes, and egui's hit test hands
267 // an overlapped point to the LAST widget registered there, so every control
268 // inside the row (the collapse box, the label, whatever `add_right` draws)
269 // still takes its own clicks and the band receives only what none of them
270 // wanted. Allocating it AFTER the row instead would invert that and the
271 // band would eat the delete button.
272 let outer = ui.scope_builder(
273 egui::UiBuilder::new().sense(egui::Sense::click_and_drag()),
274 |ui| {
275 ui.horizontal(|ui| {
276 // Zero the inter-item spacing so the geometry math is exact; gaps are
277 // added explicitly below.
278 ui.spacing_mut().item_spacing.x = 0.0;
279 if indent > 0.0 {
280 ui.add_space(indent);
281 }
282
283 // --- collapse box (expandable) or an equally-wide empty slot (leaf) ----
284 let (brect, bresp) = ui.allocate_exact_size(
285 crate::viewport::touch::target_size(ui.ctx(), egui::vec2(BOX, BOX)),
286 if row.expandable {
287 egui::Sense::click()
288 } else {
289 egui::Sense::hover()
290 },
291 );
292 box_rect = brect;
293 if row.expandable {
294 let painted = egui::Rect::from_center_size(brect.center(), egui::Vec2::splat(BOX));
295 paint_box(ui, painted, row.expanded);
296 if bresp.clicked() {
297 toggled = true;
298 }
299 }
300 ui.add_space(GAP);
301
302 // --- optional per-type glyph ------------------------------------------
303 // Drawn as catalogued artwork, sized to the row's text. Monochrome
304 // artwork is tinted to the row's text colour, so it reads exactly as the
305 // old font glyph did; colour artwork is left alone. Anything
306 // uncatalogued falls back to text.
307 if let Some(glyph) = row.glyph {
308 match crate::icons::artwork(glyph) {
309 Some(icon) => {
310 // A tree row can be the first thing on screen to draw an
311 // SVG; `install_image_loaders` is idempotent.
312 egui_extras::install_image_loaders(ui.ctx());
313 let height = ui.text_style_height(&egui::TextStyle::Body);
314 let mut art = crate::icon_text::image(icon, height);
315 if icon.mono {
316 art = art.tint(ui.visuals().text_color());
317 }
318 ui.add(art);
319 }
320 None => {
321 ui.label(egui::RichText::new(glyph).color(ui.visuals().text_color()));
322 }
323 }
324 ui.add_space(GAP);
325 }
326
327 // --- label (the click / drag handle) ----------------------------------
328 let mut text = egui::RichText::new(row.label);
329 if row.selected {
330 text = text.strong();
331 }
332 if let Some(tint) = row.tint {
333 text = text.color(tint);
334 }
335 // Click (and, on a reorder handle, drag) but NOT focus: the row's Tab
336 // stop is the band, which covers the whole row rather than just this
337 // text. Two focusable widgets per row would make Tab visit every row
338 // twice and put the keyboard's idea of "the row" on its label.
339 let sense = if row.draggable {
340 egui::Sense::CLICK | egui::Sense::DRAG
341 } else {
342 egui::Sense::CLICK
343 };
344 // A DRAGGABLE row is a reorder handle, so its label must NOT be
345 // text-selectable: egui labels are selectable by default, and a press on a
346 // selectable label anchors a text selection that the ensuing reorder drag
347 // then SWEEPS across every row the cursor passes over (the passed-over rows
348 // "light up"). Making only the drag handle non-selectable means the reorder
349 // press never anchors a selection, so no sweep — while every non-draggable
350 // row (fields, groups, scene/settings leaves) keeps default selectable text.
351 // Hover coloring is unaffected: it is computed from the response independent
352 // of `selectable` (see egui `Label`), so ordinary non-drag hover is unchanged.
353 // A label can carry catalogued glyphs of its own — the sketch panel
354 // appends ⛓ / ◐ / ⏚ marks to entity rows — and with no icon font left,
355 // drawing those as text would render a box. `icon_label` draws them from
356 // the same SVG catalog the glyph column uses.
357 //
358 // A DRAGGABLE row keeps the plain `Label`: it is the reorder handle, and
359 // it must stay ONE widget whose response is the drag. Nothing needs
360 // both — a draggable row's glyph goes in the glyph column above, not
361 // inline — and `has_icon` is false for those labels, so this only ever
362 // takes the icon path for the rows that need it.
363 let has_icon = row.label.chars().any(crate::icons::has);
364
365 // --- right-aligned content, THEN the label ----------------------------
366 // The right content is allocated FIRST, from the panel's right edge, and
367 // the label truncates into whatever it leaves — the order the form's
368 // reference lines and the form view's title row already use, for the
369 // same reason. A row's label is not a UI string the app chose: a scene
370 // row is labelled with a SOLID's name, as long as the modelling history
371 // made it. Laid out label-first, at its natural width, a 76-character
372 // name pushed the visibility checkbox 202 pt past the edge of a 320 pt
373 // panel — off the panel, where an ancestor clip eats the click too.
374 //
375 // Both label flavours take a wrap mode, so neither can overflow: the
376 // plain `Label` is the drag handle and keeps its `sense`, and `IconText`
377 // truncates the same way (a row whose label carries ⛓ / ◐ / ⏚ marks).
378 // A label SHORTER than the band still measures its own text, which is
379 // exactly why the row's click target and published rect are the BAND
380 // and not this: a two-word name left four fifths of its row dead.
381 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
382 ui.add_space(RIGHT_PAD);
383 add_right(ui);
384 // What the CALLER put at the row's right edge, as one rect.
385 // A right-to-left `Ui` grows leftwards from the panel edge,
386 // so its min_rect here — after the gutter and the caller's
387 // controls, before the label — is exactly that strip.
388 controls = ui.min_rect();
389 ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
390 label_resp = Some(if has_icon && !row.draggable {
391 IconText::new(text)
392 .sense(sense)
393 .selectable(false)
394 .truncate()
395 .show(ui)
396 } else {
397 ui.add(
398 egui::Label::new(text)
399 .sense(sense)
400 .selectable(!row.draggable)
401 .truncate(),
402 )
403 });
404 });
405 });
406 })
407 },
408 );
409 let band = outer.response;
410 // A row under the pointer takes a faint band, so a long tree shows which
411 // row a click will land on before the click (ui-polish-2026-10-28 row 13).
412 // Painted into the selection band's own slot: no layout moves, and a
413 // selected row keeps its selection colour.
414 if !row.highlighted && !row.root && band.contains_pointer() && ui.ctx().dragged_id().is_none() {
415 ui.painter().set(
416 highlight,
417 egui::epaint::RectShape::filled(band.rect, 0.0, hover_band_fill(ui.visuals())),
418 );
419 }
420 if row.highlighted && !row.root {
421 ui.painter().set(
422 highlight,
423 egui::epaint::RectShape::filled(
424 band.rect,
425 0.0,
426 ui.visuals().selection.bg_fill.gamma_multiply(0.35),
427 ),
428 );
429 }
430
431 let row_rect = outer.inner.response.rect;
432 if !row.root {
433 paint_connectors(ui, row_rect, row.guides, connector_col, row.is_last);
434 }
435 // The row is the keyboard stop, so say so when it holds the focus: nothing
436 // else in the row paints one now that the label is not focusable, and a
437 // focus nobody can see is a focus nobody can use.
438 if band.has_focus() {
439 ui.painter().rect_stroke(
440 row_rect,
441 2.0,
442 ui.visuals().selection.stroke,
443 egui::StrokeKind::Inside,
444 );
445 }
446
447 // A band drag whose press STARTED on one of the row's own controls is that
448 // control's press, not a row drag. egui reports both — a click-sensing
449 // button on top and the drag-sensing band beneath are "interested in
450 // different things", so `hit_test` hands back both hits — and without this
451 // the edit button became a reorder handle the moment the hand moved.
452 let press = ui.input(|i| i.pointer.press_origin());
453 let from_a_control = press
454 .is_some_and(|at| box_rect.contains(at) || controls.contains(at));
455 let band_drag_started = band.drag_started() && !from_a_control;
456
457 NodeResponse {
458 row_rect,
459 box_rect,
460 toggled,
461 label: label_resp.expect("label always drawn"),
462 band,
463 band_drag_started,
464 }
465}
466
467/// Draw a LEAF whose content is a multi-line, WRAPPING colored message — e.g. a
468/// feature's error text shown under its header. Unlike [`node`] (a single
469/// non-wrapping row), the label WRAPS to the available width and the row grows as
470/// tall as it needs; the `└`/`├` connector anchors to the FIRST line so it still
471/// reads as a normal child leaf under its parent. Returns the full row rect.
472pub fn message_leaf(
473 ui: &mut egui::Ui,
474 guides: &[bool],
475 is_last: bool,
476 text: &str,
477 color: egui::Color32,
478) -> egui::Rect {
479 let connector_col = guides.len();
480 // Align the wrapped text with where a LEAF's label starts: indent to the box
481 // slot column, then clear the (empty) box slot + gap.
482 let indent = (connector_col as f32 + 1.0) * INDENT + BOX + GAP;
483 let line_h = ui.text_style_height(&egui::TextStyle::Body);
484 let inner = ui.horizontal(|ui| {
485 ui.spacing_mut().item_spacing.x = 0.0;
486 ui.add_space(indent);
487 // A vertical child claims the remaining row; the wrapping label wraps to its
488 // width (minus the right gutter so it clears the sidebar scroll bar).
489 ui.vertical(|ui| {
490 ui.set_max_width((ui.available_width() - RIGHT_PAD).max(40.0));
491 ui.add(
492 egui::Label::new(egui::RichText::new(text).color(color))
493 .wrap_mode(egui::TextWrapMode::Wrap),
494 );
495 });
496 });
497 let row_rect = inner.response.rect;
498 let tick_y = row_rect.top() + line_h * 0.5;
499 paint_connectors_at(ui, row_rect, guides, connector_col, is_last, tick_y);
500 row_rect
501}
502
503/// Paint a `[+]`/`[-]` collapse box: a rounded stroked square with a minus bar
504/// (expanded) plus a vertical bar (collapsed → a plus). Theme-aware.
505fn paint_box(ui: &egui::Ui, rect: egui::Rect, expanded: bool) {
506 let painter = ui.painter();
507 let stroke = egui::Stroke::new(1.0, line_color(ui));
508 painter.rect(
509 rect,
510 egui::CornerRadius::same(2),
511 egui::Color32::TRANSPARENT,
512 stroke,
513 egui::StrokeKind::Inside,
514 );
515 let c = rect.center();
516 let arm = rect.width() * 0.28;
517 let sign = egui::Stroke::new(1.4, ui.visuals().text_color());
518 // horizontal bar (always → the minus of a `[-]`)
519 painter.line_segment(
520 [egui::pos2(c.x - arm, c.y), egui::pos2(c.x + arm, c.y)],
521 sign,
522 );
523 // vertical bar (only when collapsed → completes the plus of a `[+]`)
524 if !expanded {
525 painter.line_segment(
526 [egui::pos2(c.x, c.y - arm), egui::pos2(c.x, c.y + arm)],
527 sign,
528 );
529 }
530}
531
532/// Paint the ancestor guide lines + this node's `├`/`└` connector into the row's
533/// left gutter (over-drawn by the inter-row spacing so verticals join seamlessly
534/// across rows). The `├`/`└` tick lands at the row's vertical center.
535fn paint_connectors(
536 ui: &egui::Ui,
537 row: egui::Rect,
538 guides: &[bool],
539 connector_col: usize,
540 is_last: bool,
541) {
542 paint_connectors_at(ui, row, guides, connector_col, is_last, row.center().y);
543}
544
545/// Like [`paint_connectors`] but with an explicit `tick_y` for the `├`/`└` join —
546/// so a MULTI-LINE row (a wrapping message leaf) can anchor its connector to its
547/// FIRST line instead of the block's vertical center.
548fn paint_connectors_at(
549 ui: &egui::Ui,
550 row: egui::Rect,
551 guides: &[bool],
552 connector_col: usize,
553 is_last: bool,
554 mid: f32,
555) {
556 let painter = ui.painter();
557 let stroke = egui::Stroke::new(1.0, line_color(ui));
558 let sp = ui.spacing().item_spacing.y + 1.0;
559 let left = row.left();
560 let top = row.top() - sp;
561 let bottom = row.bottom() + sp;
562 let col_x = |c: usize| left + c as f32 * INDENT + INDENT * 0.5;
563
564 // Ancestor sibling lines passing through this row.
565 for (i, on) in guides.iter().enumerate() {
566 if *on {
567 let x = col_x(i);
568 painter.line_segment([egui::pos2(x, top), egui::pos2(x, bottom)], stroke);
569 }
570 }
571 // This node's own connector: vertical down to mid (└) or through (├), plus a
572 // horizontal tick reaching the box slot.
573 let x = col_x(connector_col);
574 let v_bottom = if is_last { mid } else { bottom };
575 painter.line_segment([egui::pos2(x, top), egui::pos2(x, v_bottom)], stroke);
576 painter.line_segment(
577 [egui::pos2(x, mid), egui::pos2(x + INDENT * 0.5, mid)],
578 stroke,
579 );
580}
581
582/// The subtle connector / box stroke color — the theme's non-interactive
583/// foreground, dimmed. Theme-aware (light + dark) and the seam the later theme
584/// pass tunes in one place.
585fn line_color(ui: &egui::Ui) -> egui::Color32 {
586 ui.visuals()
587 .widgets
588 .noninteractive
589 .fg_stroke
590 .color
591 .gamma_multiply(0.7)
592}
593