git.lucas.co / cce-ui
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 }