agg_gui/widgets/menu/
strip.rs1use crate::draw_ctx::DrawCtx;
25use crate::event::{Event, EventResult, MouseButton};
26use crate::geometry::{Rect, Size};
27use crate::widget::{current_mouse_world, Widget};
28
29pub struct MenuBarStrip {
30 bounds: Rect,
31 children: Vec<Box<dyn Widget>>,
32 h_offset: f64,
33 content_width: f64,
34 content_height: f64,
35 middle_dragging: bool,
36 middle_start_world_x: f64,
37 middle_start_h_offset: f64,
38}
39
40impl MenuBarStrip {
41 pub fn new(inner: Box<dyn Widget>) -> Self {
42 Self {
43 bounds: Rect::default(),
44 children: vec![inner],
45 h_offset: 0.0,
46 content_width: 0.0,
47 content_height: 0.0,
48 middle_dragging: false,
49 middle_start_world_x: 0.0,
50 middle_start_h_offset: 0.0,
51 }
52 }
53
54 fn max_scroll(&self) -> f64 {
55 (self.content_width - self.bounds.width).max(0.0)
56 }
57
58 fn clamp_offset(&mut self) {
59 self.h_offset = self.h_offset.clamp(0.0, self.max_scroll());
60 }
61
62 fn update_child_bounds(&mut self) {
63 if let Some(child) = self.children.first_mut() {
64 child.set_bounds(Rect::new(
65 -self.h_offset.round(),
66 0.0,
67 self.content_width,
68 self.content_height,
69 ));
70 }
71 }
72}
73
74impl Widget for MenuBarStrip {
75 fn type_name(&self) -> &'static str {
76 "MenuBarStrip"
77 }
78
79 fn bounds(&self) -> Rect {
80 self.bounds
81 }
82
83 fn set_bounds(&mut self, bounds: Rect) {
84 self.bounds = bounds;
85 }
86
87 fn children(&self) -> &[Box<dyn Widget>] {
88 &self.children
89 }
90
91 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
92 &mut self.children
93 }
94
95 fn layout(&mut self, available: Size) -> Size {
96 let used = if let Some(child) = self.children.first_mut() {
102 child.layout(available)
103 } else {
104 Size::new(0.0, 0.0)
105 };
106 self.content_height = used.height;
107 self.content_width = used.width.max(available.width);
108 self.bounds = Rect::new(0.0, 0.0, available.width, self.content_height);
109 self.clamp_offset();
110 self.update_child_bounds();
111 Size::new(available.width, self.content_height)
112 }
113
114 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
115 let v = ctx.visuals();
116 ctx.set_fill_color(v.top_bar_bg);
117 ctx.begin_path();
118 ctx.rect(0.0, 0.0, self.bounds.width, self.bounds.height);
119 ctx.fill();
120 }
121
122 fn paint_overlay(&mut self, ctx: &mut dyn DrawCtx) {
123 let v = ctx.visuals();
130 ctx.set_fill_color(v.separator);
131 ctx.begin_path();
132 ctx.rect(0.0, 0.0, self.bounds.width, 1.0);
133 ctx.fill();
134 }
135
136 fn on_event(&mut self, event: &Event) -> EventResult {
137 match event {
138 Event::MouseWheel {
139 delta_x,
140 delta_y,
141 modifiers,
142 ..
143 } => {
144 let delta = if delta_x.abs() > f64::EPSILON {
148 *delta_x
149 } else if modifiers.shift {
150 *delta_y
151 } else {
152 0.0
153 };
154 if delta.abs() <= f64::EPSILON || self.max_scroll() <= 0.0 {
155 return EventResult::Ignored;
156 }
157 let before = self.h_offset;
158 self.h_offset += delta * 40.0;
159 self.clamp_offset();
160 if (self.h_offset - before).abs() > f64::EPSILON {
161 self.update_child_bounds();
162 crate::animation::request_draw();
163 return EventResult::Consumed;
164 }
165 EventResult::Ignored
166 }
167 Event::MouseDown {
168 button: MouseButton::Middle,
169 ..
170 } if self.max_scroll() > 0.0 => {
171 self.middle_dragging = true;
172 self.middle_start_world_x = current_mouse_world().map(|p| p.x).unwrap_or(0.0);
173 self.middle_start_h_offset = self.h_offset;
174 EventResult::Consumed
175 }
176 Event::MouseMove { pos } if self.middle_dragging => {
177 let world_x = current_mouse_world().map(|p| p.x).unwrap_or(pos.x);
178 self.h_offset = self.middle_start_h_offset - (world_x - self.middle_start_world_x);
179 self.clamp_offset();
180 self.update_child_bounds();
181 crate::animation::request_draw();
182 EventResult::Consumed
183 }
184 Event::MouseUp {
185 button: MouseButton::Middle,
186 ..
187 } if self.middle_dragging => {
188 self.middle_dragging = false;
189 EventResult::Consumed
190 }
191 _ => EventResult::Ignored,
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::geometry::Rect;
200 use crate::widget::Widget;
201
202 struct FixedHeightChild {
206 bounds: Rect,
207 children: Vec<Box<dyn Widget>>,
208 natural_height: f64,
209 natural_width: f64,
210 }
211
212 impl Widget for FixedHeightChild {
213 fn type_name(&self) -> &'static str {
214 "FixedHeightChild"
215 }
216 fn bounds(&self) -> Rect {
217 self.bounds
218 }
219 fn set_bounds(&mut self, b: Rect) {
220 self.bounds = b;
221 }
222 fn children(&self) -> &[Box<dyn Widget>] {
223 &self.children
224 }
225 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
226 &mut self.children
227 }
228 fn layout(&mut self, _available: Size) -> Size {
229 Size::new(self.natural_width, self.natural_height)
230 }
231 fn paint(&mut self, _ctx: &mut dyn crate::DrawCtx) {}
232 fn on_event(&mut self, _: &crate::Event) -> crate::EventResult {
233 crate::EventResult::Ignored
234 }
235 }
236
237 #[test]
238 fn strip_height_matches_child_natural_height() {
239 let child = FixedHeightChild {
240 bounds: Rect::default(),
241 children: Vec::new(),
242 natural_height: 26.0,
243 natural_width: 120.0,
244 };
245 let mut strip = MenuBarStrip::new(Box::new(child));
246 let used = strip.layout(Size::new(800.0, 200.0));
247 assert_eq!(used.height, 26.0, "strip must size to child's height");
248 assert_eq!(used.width, 800.0, "strip must span full available width");
249 }
250
251 #[test]
252 fn strip_overflow_scroll_kicks_in_when_child_wider_than_available() {
253 let child = FixedHeightChild {
254 bounds: Rect::default(),
255 children: Vec::new(),
256 natural_height: 26.0,
257 natural_width: 1000.0,
258 };
259 let mut strip = MenuBarStrip::new(Box::new(child));
260 strip.layout(Size::new(400.0, 200.0));
261 assert!(
262 strip.max_scroll() > 0.0,
263 "child wider than available width must produce scrollable overflow"
264 );
265 }
266}