Skip to main content

agg_gui/widgets/
splitter.rs

1//! `Splitter` — draggable divider between two side-by-side children.
2//!
3//! Phase 5: horizontal split only (left panel | right panel).
4
5use crate::draw_ctx::DrawCtx;
6use crate::event::{Event, EventResult, MouseButton};
7use crate::geometry::{Point, Rect, Size};
8use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
9use crate::widget::Widget;
10
11/// A draggable divider that splits its two children horizontally.
12///
13/// `children[0]` = left panel, `children[1]` = right panel.
14pub struct Splitter {
15    bounds: Rect,
16    children: Vec<Box<dyn Widget>>, // exactly 2
17    base: WidgetBase,
18    /// Split position as a fraction of total width. Clamped to [0.05, 0.95].
19    pub ratio: f64,
20    /// Width of the draggable divider strip.
21    pub divider_width: f64,
22
23    hovered: bool,
24    dragging: bool,
25}
26
27impl Splitter {
28    pub fn new(left: Box<dyn Widget>, right: Box<dyn Widget>) -> Self {
29        Self {
30            bounds: Rect::default(),
31            children: vec![left, right],
32            base: WidgetBase::new(),
33            ratio: 0.5,
34            divider_width: 6.0,
35            hovered: false,
36            dragging: false,
37        }
38    }
39
40    pub fn with_ratio(mut self, ratio: f64) -> Self {
41        self.ratio = ratio.clamp(0.05, 0.95);
42        self
43    }
44
45    pub fn with_divider_width(mut self, w: f64) -> Self {
46        self.divider_width = w;
47        self
48    }
49
50    pub fn with_margin(mut self, m: Insets) -> Self {
51        self.base.margin = m;
52        self
53    }
54    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
55        self.base.h_anchor = h;
56        self
57    }
58    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
59        self.base.v_anchor = v;
60        self
61    }
62    pub fn with_min_size(mut self, s: Size) -> Self {
63        self.base.min_size = s;
64        self
65    }
66    pub fn with_max_size(mut self, s: Size) -> Self {
67        self.base.max_size = s;
68        self
69    }
70
71    fn divider_x(&self) -> f64 {
72        (self.bounds.width - self.divider_width) * self.ratio
73    }
74}
75
76impl Widget for Splitter {
77    fn type_name(&self) -> &'static str {
78        "Splitter"
79    }
80    fn bounds(&self) -> Rect {
81        self.bounds
82    }
83    fn set_bounds(&mut self, b: Rect) {
84        self.bounds = b;
85    }
86    fn children(&self) -> &[Box<dyn Widget>] {
87        &self.children
88    }
89    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
90        &mut self.children
91    }
92
93    fn margin(&self) -> Insets {
94        self.base.margin
95    }
96    fn h_anchor(&self) -> HAnchor {
97        self.base.h_anchor
98    }
99    fn v_anchor(&self) -> VAnchor {
100        self.base.v_anchor
101    }
102    fn min_size(&self) -> Size {
103        self.base.min_size
104    }
105    fn max_size(&self) -> Size {
106        self.base.max_size
107    }
108
109    fn hit_test(&self, local_pos: Point) -> bool {
110        // Capture all events during drag, even if cursor leaves bounds.
111        if self.dragging {
112            return true;
113        }
114        let b = self.bounds();
115        local_pos.x >= 0.0
116            && local_pos.x <= b.width
117            && local_pos.y >= 0.0
118            && local_pos.y <= b.height
119    }
120
121    fn layout(&mut self, available: Size) -> Size {
122        let div = self.divider_width;
123        let left_w = ((available.width - div) * self.ratio).max(0.0);
124        let right_w = (available.width - div - left_w).max(0.0);
125        let h = available.height;
126
127        if self.children.len() >= 2 {
128            self.children[0].layout(Size::new(left_w, h));
129            self.children[0].set_bounds(Rect::new(0.0, 0.0, left_w, h));
130
131            let right_x = left_w + div;
132            self.children[1].layout(Size::new(right_w, h));
133            self.children[1].set_bounds(Rect::new(right_x, 0.0, right_w, h));
134        }
135
136        available
137    }
138
139    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
140        let v = ctx.visuals();
141        let div_x = self.divider_x();
142        let h = self.bounds.height;
143
144        let color = if self.dragging {
145            v.accent.with_alpha(0.6)
146        } else if self.hovered {
147            v.text_color.with_alpha(0.15)
148        } else {
149            v.text_color.with_alpha(0.08)
150        };
151        ctx.set_fill_color(color);
152        ctx.begin_path();
153        ctx.rect(div_x, 0.0, self.divider_width, h);
154        ctx.fill();
155
156        // Grip dots in the center of the divider
157        if h > 30.0 {
158            let grip_color = if self.hovered || self.dragging {
159                v.accent.with_alpha(0.7)
160            } else {
161                v.text_color.with_alpha(0.25)
162            };
163            ctx.set_fill_color(grip_color);
164            let cx = div_x + self.divider_width * 0.5;
165            let cy = h * 0.5;
166            for i in -1i32..=1 {
167                ctx.begin_path();
168                ctx.circle(cx, cy + i as f64 * 5.0, 1.5);
169                ctx.fill();
170            }
171        }
172    }
173
174    fn on_event(&mut self, event: &Event) -> EventResult {
175        let div_x = self.divider_x();
176        let div_end = div_x + self.divider_width;
177
178        match event {
179            Event::MouseMove { pos } => {
180                let over_div = pos.x >= div_x - 2.0 && pos.x <= div_end + 2.0;
181                let was = self.hovered;
182                self.hovered = over_div;
183                if self.dragging {
184                    let total = self.bounds.width;
185                    if total > self.divider_width {
186                        self.ratio = (pos.x / total).clamp(0.05, 0.95);
187                    }
188                    crate::animation::request_draw();
189                    EventResult::Consumed
190                } else {
191                    if was != self.hovered {
192                        crate::animation::request_draw();
193                        return EventResult::Consumed;
194                    }
195                    EventResult::Ignored
196                }
197            }
198            Event::MouseDown {
199                pos,
200                button: MouseButton::Left,
201                ..
202            } => {
203                if pos.x >= div_x - 2.0 && pos.x <= div_end + 2.0 {
204                    self.dragging = true;
205                    // No tick: `dragging = true` produces no immediate
206                    // visible change.  Subsequent MouseMove deltas will
207                    // tick as the split ratio actually shifts.
208                    EventResult::Consumed
209                } else {
210                    EventResult::Ignored
211                }
212            }
213            Event::MouseUp {
214                button: MouseButton::Left,
215                ..
216            } => {
217                let was_dragging = self.dragging;
218                self.dragging = false;
219                if was_dragging {
220                    crate::animation::request_draw();
221                    EventResult::Consumed
222                } else {
223                    EventResult::Ignored
224                }
225            }
226            _ => EventResult::Ignored,
227        }
228    }
229}