Skip to main content

agg_gui/widgets/
chevron.rs

1//! `ChevronWidget` — a small clickable arrow that toggles a
2//! collapsed/expanded state. Composes into title bars, accordion
3//! headers, tree rows, anywhere a "fold" affordance is needed.
4//!
5//! The chevron is a real `Widget`: it has its own bounds, paints
6//! itself, and consumes mouse-down events that land inside it. The
7//! parent uses standard `children_mut()` + `layout()` to place it,
8//! and supplies an `on_click` closure to act on the toggle. Parents
9//! that need to share collapse state across multiple widgets pass an
10//! `Rc<Cell<bool>>` for the chevron to read each paint — keeping a
11//! single source of truth without copy-on-every-frame boilerplate.
12
13use std::cell::Cell;
14use std::rc::Rc;
15
16use crate::color::Color;
17use crate::draw_ctx::DrawCtx;
18use crate::event::{Event, EventResult, MouseButton};
19use crate::geometry::{Point, Rect, Size};
20use crate::layout_props::WidgetBase;
21use crate::widget::Widget;
22use crate::widgets::window::chrome::paint_chevron;
23
24/// Logical size of the chevron's hit / paint region. The arrow itself
25/// is ~8 px wide; the surrounding padding gives the user a comfortable
26/// click target.
27pub const CHEVRON_SIZE: f64 = 16.0;
28
29/// A clickable collapse / expand chevron.
30pub struct ChevronWidget {
31    bounds: Rect,
32    base: WidgetBase,
33    children: Vec<Box<dyn Widget>>,
34    /// Shared collapse flag — the chevron reads this each paint to pick
35    /// its glyph orientation. The parent writes it when the user (or
36    /// any other code path) toggles the fold.
37    collapsed: Rc<Cell<bool>>,
38    /// Shared glyph colour cell — the parent writes the theme colour
39    /// each paint pass; the chevron reads it without needing a typed
40    /// downcast through the children Vec. Defaults to white so a
41    /// caller that never wires the cell still gets a visible glyph.
42    color: Rc<Cell<Color>>,
43    /// Shared "affordance active" flag. When `false` the chevron paints
44    /// nothing and ignores clicks — lets a parent hide the collapse
45    /// affordance at runtime (e.g. a Window whose `collapsible` flag is
46    /// toggled off) without rebuilding the widget tree. Defaults to `true`.
47    enabled: Rc<Cell<bool>>,
48    /// Invoked on left-click. Parents put their toggle logic in here.
49    on_click: Option<Box<dyn FnMut()>>,
50}
51
52impl ChevronWidget {
53    /// Build a chevron sharing `collapsed` with its parent. The parent
54    /// is the source of truth — the chevron only renders + emits clicks.
55    pub fn new(collapsed: Rc<Cell<bool>>) -> Self {
56        Self {
57            bounds: Rect::new(0.0, 0.0, CHEVRON_SIZE, CHEVRON_SIZE),
58            base: WidgetBase::new(),
59            children: Vec::new(),
60            collapsed,
61            color: Rc::new(Cell::new(Color::white())),
62            enabled: Rc::new(Cell::new(true)),
63            on_click: None,
64        }
65    }
66
67    /// Share the "affordance active" cell with the parent. When the parent
68    /// sets it to `false`, the chevron becomes invisible and non-interactive.
69    pub fn with_enabled_cell(mut self, cell: Rc<Cell<bool>>) -> Self {
70        self.enabled = cell;
71        self
72    }
73
74    /// Wire a left-click handler. Typical implementations flip the
75    /// shared `collapsed` cell and request a redraw / notify their
76    /// owner. Builder form — chain at construction.
77    pub fn on_click(mut self, f: impl FnMut() + 'static) -> Self {
78        self.on_click = Some(Box::new(f));
79        self
80    }
81
82    /// Hand a shared colour cell to the chevron. The parent keeps a
83    /// clone of the returned cell and writes the active theme colour
84    /// into it each paint pass; the chevron picks it up automatically.
85    pub fn with_color_cell(mut self, c: Rc<Cell<Color>>) -> Self {
86        self.color = c;
87        self
88    }
89}
90
91impl Widget for ChevronWidget {
92    fn type_name(&self) -> &'static str {
93        "ChevronWidget"
94    }
95    fn bounds(&self) -> Rect {
96        self.bounds
97    }
98    fn set_bounds(&mut self, b: Rect) {
99        self.bounds = b;
100    }
101    fn children(&self) -> &[Box<dyn Widget>] {
102        &self.children
103    }
104    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
105        &mut self.children
106    }
107    fn widget_base(&self) -> Option<&WidgetBase> {
108        Some(&self.base)
109    }
110
111    fn layout(&mut self, _available: Size) -> Size {
112        Size::new(self.bounds.width, self.bounds.height)
113    }
114
115    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
116        if !self.enabled.get() {
117            return;
118        }
119        let cx = self.bounds.width * 0.5;
120        let cy = self.bounds.height * 0.5;
121        paint_chevron(ctx, cx, cy, self.collapsed.get(), self.color.get());
122    }
123
124    fn hit_test(&self, local: Point) -> bool {
125        self.enabled.get()
126            && local.x >= 0.0
127            && local.x <= self.bounds.width
128            && local.y >= 0.0
129            && local.y <= self.bounds.height
130    }
131
132    fn on_event(&mut self, event: &Event) -> EventResult {
133        if !self.enabled.get() {
134            return EventResult::Ignored;
135        }
136        if let Event::MouseDown {
137            button: MouseButton::Left,
138            ..
139        } = event
140        {
141            if let Some(cb) = self.on_click.as_mut() {
142                cb();
143            }
144            crate::animation::request_draw();
145            return EventResult::Consumed;
146        }
147        EventResult::Ignored
148    }
149}