GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/widget/display/info_box.rs (5.4K)
1 //! Narrow-trait info box (Phase 5i leaf sweep). Pure display: themed card + border + title/lines.
2
3 use crate::colors;
4 use crate::scene::layout::Rect;
5 use crate::scene::paint::PaintCtx;
6 use crate::widget::{Adapted, Input, Layout, Paint};
7
8 #[derive(Debug, Clone)]
9 pub struct InfoBox {
10 pub title: String,
11 pub lines: Vec<String>,
12 }
13
14 impl InfoBox {
15 pub fn new(title: &str, lines: Vec<String>) -> Adapted<InfoBox> {
16 Adapted::new(InfoBox { title: title.to_string(), lines })
17 }
18 }
19
20 impl Layout for InfoBox {
21 fn inline_label(&self) -> bool {
22 true // draws its own title text
23 }
24 }
25
26 impl Paint for InfoBox {
27 fn color(&self) -> [f32; 4] {
28 [0.0, 0.0, 0.0, 0.0]
29 }
30
31 fn widget_font(&self) -> Option<String> {
32 Some(crate::layout::control_label_font())
33 }
34
35 fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
36 let (x, y) = (rect.x, rect.y);
37 let r = crate::layout::plate_corner_radius();
38 let radii = (r, r, r, r);
39 if crate::layout::control_relief() {
40 // A pane plate, raised: the DE plate fill under a rolled edge —
41 // the same surface a popover or a menu stands on. Its roll is the
42 // control wall, capped by its height like every plate's.
43 let fill = colors::plate_color().unwrap_or_else(|| colors::active_theme().surface_bg);
44 let depth = crate::layout::bevel_width().min(rect.height * 0.2);
45 ctx.plate(rect, radii, &crate::scene::material::Material::from_fill(fill), depth);
46 } else {
47 // Flat: the themed surface in a hairline frame.
48 let theme = colors::active_theme();
49 ctx.border(rect, radii, theme.surface_bg, theme.surface_border, 1.0);
50 }
51
52 // The title in the DE highlight accent, the lines in the label colour;
53 // sizes from the label font so the box reads like the controls around it.
54 let (_, font_size) = crate::layout::control_label_font_parsed();
55 let hc = colors::to_srgb(crate::color::highlight_primary_color());
56 let title_color = [(hc[0] * 255.0).round() as u8, (hc[1] * 255.0).round() as u8, (hc[2] * 255.0).round() as u8];
57 let line_color = colors::control_label_color_u8();
58 let pad = crate::layout::plate_padding().max(8.0);
59 let line_h = crate::layout::line_height(font_size);
60 // Clipped to the box. The title and lines are caller-supplied text in
61 // a box the caller also sizes, so nothing here guarantees they fit.
62 let clip = Some([x + pad, y, x + rect.width - pad, y + rect.height]);
63 ctx.text_with(self.title.clone(), x + pad, y + pad, font_size, title_color, None, clip);
64 let mut current_y = y + pad + line_h * 1.4;
65 for line in &self.lines {
66 ctx.text_with(line.clone(), x + pad, current_y, font_size, line_color, None, clip);
67 current_y += line_h;
68 }
69 }
70 }
71
72 impl Input for InfoBox {}
73
74
75 #[cfg(test)]
76 mod bounded_text_audit {
77 use crate::scene::layout::Rect;
78 use crate::scene::paint::{PaintCtx, Prim};
79 use crate::widget::Paint;
80
81 /// Every string a widget paints must carry a clip, so that a value longer
82 /// than the box it was given is cut at the box instead of drawn across
83 /// whatever sits beside it. This is the invariant the whole audit was
84 /// about; it is asserted here over a sample of widgets rather than in each
85 /// of their files so that a NEW widget drawing unbounded text trips it.
86 ///
87 /// The one deliberate exception is `Node`, which draws its name in the
88 /// gutter beside itself and cannot know how much gutter it has — see the
89 /// comment there. It is excluded on purpose, not forgotten.
90 fn unbounded_strings<F: FnOnce(&mut PaintCtx)>(paint: F) -> Vec<String> {
91 let mut pc = PaintCtx::new();
92 paint(&mut pc);
93 pc.finish()
94 .items
95 .iter()
96 .filter_map(|i| match &i.prim {
97 Prim::Text { text, bounds: None, .. } => Some(text.clone()),
98 _ => None,
99 })
100 .collect()
101 }
102
103 /// A box narrower than any of its content — the shape that used to spill.
104 const TIGHT: Rect = Rect { x: 40.0, y: 10.0, width: 50.0, height: 28.0 };
105
106 #[test]
107 fn info_box_text_is_bounded() {
108 let b = super::InfoBox::new(
109 "A title far wider than fifty pixels",
110 vec!["and a line wider still, by some margin".to_string()],
111 );
112 let loose = unbounded_strings(|pc| Paint::paint(&*b, TIGHT, pc));
113 assert!(loose.is_empty(), "InfoBox drew unbounded text: {loose:?}");
114 }
115
116 #[test]
117 fn label_text_is_bounded() {
118 let l = crate::widget::Label::new("a label considerably wider than its box");
119 let loose = unbounded_strings(|pc| Paint::paint(&*l, TIGHT, pc));
120 assert!(loose.is_empty(), "Label drew unbounded text: {loose:?}");
121 }
122
123 #[test]
124 fn slider_readout_is_bounded() {
125 let s = crate::widget::Slider::new();
126 let loose = unbounded_strings(|pc| Paint::paint(&*s, TIGHT, pc));
127 assert!(loose.is_empty(), "Slider drew unbounded text: {loose:?}");
128 }
129
130 #[test]
131 fn checkbox_label_is_bounded() {
132 let mut c = crate::widget::Checkbox::new();
133 c.set_text("a checkbox label much wider than fifty pixels");
134 let loose = unbounded_strings(|pc| Paint::paint(&*c, TIGHT, pc));
135 assert!(loose.is_empty(), "Checkbox drew unbounded text: {loose:?}");
136 }
137 }