1mod strip;
4#[cfg(test)]
5mod tests;
6
7use std::rc::Rc;
8use std::time::Duration;
9
10use crate::event::Event;
11use crate::geometry::{Rect, Size};
12use crate::keymap::Scope;
13use crate::motion::{Easing, steps};
14use crate::widget::{Axis as FlexAxis, EventCx, Flex, Length, MeasureCx, Node, NodeMut, PaintCx, View, Widget};
15
16use super::boundary::{self, Axis, Change, Toggle};
17use strip::{OpenMessage, STRIP, Strip, ViewMessage};
18
19type Part<'a, Msg> = Box<dyn FnOnce(&mut View<'_, Msg>) + 'a>;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum Side {
24 #[default]
26 Left,
27 Right,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum Closed {
34 #[default]
37 Collapse,
38 Hide,
41}
42
43pub struct SidePanel<'a, Msg> {
72 side: Side,
73 width: u16,
74 open: bool,
75 closed: Closed,
76 min: u16,
77 max: Option<u16>,
78 on_toggle: Option<OpenMessage<Msg>>,
79 on_resize: Option<Box<dyn Fn(u16) -> Msg>>,
80 strip: Vec<String>,
81 on_strip: Option<ViewMessage<Msg>>,
82 active_view: Option<u16>,
83 panel: Option<Part<'a, Msg>>,
84 body: Option<Part<'a, Msg>>,
85}
86
87impl<'a, Msg: 'static> SidePanel<'a, Msg> {
88 #[must_use]
90 pub fn new(width: u16) -> Self {
91 Self {
92 side: Side::Left,
93 width,
94 open: true,
95 closed: Closed::Collapse,
96 min: 8,
97 max: None,
98 on_toggle: None,
99 on_resize: None,
100 strip: Vec::new(),
101 on_strip: None,
102 active_view: None,
103 panel: None,
104 body: None,
105 }
106 }
107
108 #[must_use]
110 pub fn side(mut self, side: Side) -> Self {
111 self.side = side;
112 self
113 }
114
115 #[must_use]
117 pub fn open(mut self, open: bool) -> Self {
118 self.open = open;
119 self
120 }
121
122 #[must_use]
125 pub fn closed(mut self, closed: Closed) -> Self {
126 self.closed = closed;
127 self
128 }
129
130 #[must_use]
132 pub fn on_toggle(mut self, message: impl Fn(bool) -> Msg + 'static) -> Self {
133 self.on_toggle = Some(Rc::new(message));
134 self
135 }
136
137 #[must_use]
139 pub fn on_resize(mut self, message: impl Fn(u16) -> Msg + 'static) -> Self {
140 self.on_resize = Some(Box::new(message));
141 self
142 }
143
144 #[must_use]
148 pub fn limits(mut self, min: u16, max: impl Into<Option<u16>>) -> Self {
149 self.min = min;
150 self.max = max.into();
151 self
152 }
153
154 #[must_use]
159 pub fn strip(
160 mut self,
161 icons: impl IntoIterator<Item = impl Into<String>>,
162 message: impl Fn(u16) -> Msg + 'static,
163 ) -> Self {
164 self.strip = icons.into_iter().map(Into::into).collect();
165 self.on_strip = Some(Rc::new(message));
166 self
167 }
168
169 #[must_use]
172 pub fn active_view(mut self, index: u16) -> Self {
173 self.active_view = Some(index);
174 self
175 }
176
177 #[must_use]
179 pub fn panel(mut self, build: impl FnOnce(&mut View<'_, Msg>) + 'a) -> Self {
180 self.panel = Some(Box::new(build));
181 self
182 }
183
184 #[must_use]
186 pub fn body(mut self, build: impl FnOnce(&mut View<'_, Msg>) + 'a) -> Self {
187 self.body = Some(Box::new(build));
188 self
189 }
190
191 pub fn show<'v>(self, ui: &'v mut View<'_, Msg>) -> NodeMut<'v, Msg> {
193 let build = |part: Option<Part<'a, Msg>>| {
194 let mut children = Vec::new();
195 if let Some(part) = part {
196 part(&mut ui.nested(&mut children));
197 }
198 let mut node = Node::new(Flex::new(FlexAxis::Column, children), 0);
199 node.layout.width = Length::Fill(1);
200 node.layout.height = Length::Fill(1);
201 node
202 };
203 let mut parts = vec![build(self.panel), build(self.body)];
204 let has_strip = match self.on_strip {
205 Some(on_select) if !self.strip.is_empty() => {
206 let strip = Strip {
207 side: self.side,
208 icons: self.strip,
209 active: self.active_view,
210 open: self.open,
211 on_select,
212 on_toggle: self.on_toggle.clone(),
213 };
214 parts.push(Node::new(strip, 2));
215 true
216 }
217 _ => false,
218 };
219 let dock = Dock {
220 side: self.side,
221 width: self.width,
222 open: self.open,
223 closed: self.closed,
224 min: self.min,
225 max: self.max,
226 on_toggle: self.on_toggle,
227 on_resize: self.on_resize,
228 has_strip,
229 parts,
230 };
231 ui.add(dock).fill()
232 }
233}
234
235const PANEL: usize = 0;
237const BODY: usize = 1;
238const STRIP_PART: usize = 2;
239
240struct Dock<Msg> {
241 side: Side,
242 width: u16,
243 open: bool,
244 closed: Closed,
245 min: u16,
246 max: Option<u16>,
248 on_toggle: Option<OpenMessage<Msg>>,
249 on_resize: Option<Box<dyn Fn(u16) -> Msg>>,
250 has_strip: bool,
252 parts: Vec<Node<Msg>>,
253}
254
255impl<Msg: 'static> Dock<Msg> {
256 fn interactive(&self) -> bool {
257 self.on_toggle.is_some() || self.on_resize.is_some()
258 }
259
260 fn strip_width(&self) -> u16 {
262 if self.has_strip { STRIP } else { 0 }
263 }
264
265 fn collapsed_width(&self) -> u16 {
268 match self.closed {
269 Closed::Collapse => self.strip_width(),
270 Closed::Hide => 0,
271 }
272 }
273
274 fn room(&self, area: Rect) -> u16 {
276 area.width.saturating_sub(area.width / 4).saturating_sub(self.strip_width())
277 }
278
279 fn max_width(&self, area: Rect) -> u16 {
281 let room = self.room(area);
282 self.max.map_or(room, |max| max.min(room))
283 }
284
285 fn open_width(&self, area: Rect) -> u16 {
288 boundary::clamp_size(i32::from(self.width), self.min, self.max_width(area)).min(self.room(area))
289 }
290
291 fn edge_rect(&self, area: Rect, width: u16) -> Rect {
293 match self.side {
294 Side::Left => Rect::new(area.x, area.y, width, area.height),
295 Side::Right => Rect::new(area.right() - i32::from(width), area.y, width, area.height),
296 }
297 }
298
299 fn handle_rect(&self, area: Rect, width: u16) -> Rect {
302 let panel = self.edge_rect(area, width.max(1));
303 match self.side {
304 Side::Left => Rect::new(panel.right() - 1, area.y, 1, area.height),
305 Side::Right => Rect::new(panel.x, area.y, 1, area.height),
306 }
307 }
308
309 fn toggle(&self, cx: &mut EventCx<'_, Msg>) {
310 if let Some(message) = &self.on_toggle {
311 cx.emit(message(!self.open));
312 }
313 }
314
315 fn resize(&self, cx: &mut EventCx<'_, Msg>, target: i32) {
316 let area = cx.area();
317 let width = boundary::clamp_size(target, self.min, self.max_width(area));
318 if let Some(message) = &self.on_resize
319 && self.open
320 && width != self.open_width(area)
321 {
322 cx.emit(message(width));
323 }
324 }
325
326 fn drag_open(&self, cx: &mut EventCx<'_, Msg>, x: i32, target: i32) {
330 let Some(toggle) = &self.on_toggle else { return };
331 let area = cx.area();
332 let edge = self.handle_rect(area, self.collapsed_width().min(area.width)).x;
333 let past_edge = match self.side {
334 Side::Left => x - edge,
335 Side::Right => edge - x,
336 };
337 if past_edge < i32::from((self.min / 2).max(1)) {
338 return;
339 }
340 cx.emit(toggle(true));
341 if let Some(message) = &self.on_resize {
342 let width = boundary::clamp_size(target, self.min, self.max_width(area));
343 if width != self.open_width(area) {
344 cx.emit(message(width));
345 }
346 }
347 }
348
349 fn paint_sliding(&self, cx: &mut PaintCx<'_>, area: Rect, width: u16, open_width: u16) {
353 let collapsed = self.collapsed_width();
354 let visible = self.edge_rect(area, width);
355 let sliding = width.saturating_sub(collapsed);
356 let strip = self.strip_width().saturating_sub(collapsed);
357 let layer_width = strip + open_width;
358 let (moving, layer_x) = match self.side {
359 Side::Left => (
360 Rect::new(visible.x + i32::from(collapsed), area.y, sliding, area.height),
361 visible.right() - i32::from(layer_width),
362 ),
363 Side::Right => (Rect::new(visible.x, area.y, sliding, area.height), visible.x),
364 };
365 let (strip_x, panel_x) = match self.side {
366 Side::Left => (layer_x, layer_x + i32::from(strip)),
367 Side::Right => (layer_x + i32::from(open_width), layer_x),
368 };
369 let panel = Rect::new(panel_x, area.y, open_width, area.height);
370 let background = cx.style("side-panel", None, &[]).text().bg.unwrap_or_else(|| cx.color("surface"));
371 let handle = u16::from(self.interactive());
373 let (content, inside) = match self.side {
374 Side::Left => (
375 Rect::new(panel.x, area.y, open_width.saturating_sub(handle), area.height),
376 Rect::new(moving.x, area.y, sliding.saturating_sub(handle), area.height),
377 ),
378 Side::Right => (
379 Rect::new(panel.x + i32::from(handle), area.y, open_width.saturating_sub(handle), area.height),
380 Rect::new(moving.x + i32::from(handle), area.y, sliding.saturating_sub(handle), area.height),
381 ),
382 };
383 cx.with_clip(moving, |cx| cx.clear(panel, background));
384 cx.with_clip(inside, |cx| {
385 cx.paint_child(&self.parts[PANEL], content);
386 if strip > 0 {
387 cx.paint_child(&self.parts[STRIP_PART], Rect::new(strip_x, area.y, strip, area.height));
388 }
389 });
390 }
391}
392
393impl<Msg: 'static> Widget<Msg> for Dock<Msg> {
394 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
395 available
396 }
397
398 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
399 let open_width = self.open_width(area);
400 let full = (self.strip_width() + open_width).min(area.width);
401 let collapsed = self.collapsed_width().min(full);
402 let duration = cx.env().theme().motion().enter * 2;
403 let progress = cx.animate("open", if self.open { 1.0 } else { 0.0 }, duration, Easing::EaseOut);
404 let width = collapsed + steps(progress, full - collapsed);
405 let gutter = u16::from(self.interactive() && width == 0);
407
408 let body_rect = match self.side {
409 Side::Left => Rect::new(
410 area.x + i32::from(width + gutter),
411 area.y,
412 area.width.saturating_sub(width + gutter),
413 area.height,
414 ),
415 Side::Right => Rect::new(area.x, area.y, area.width.saturating_sub(width + gutter), area.height),
416 };
417 if collapsed > 0 {
419 cx.paint_child(&self.parts[STRIP_PART], self.edge_rect(area, collapsed));
420 }
421 if width > collapsed {
422 self.paint_sliding(cx, area, width, open_width);
423 } else if self.has_strip
424 && collapsed == 0
425 && self.interactive()
426 && cx.focused() == Some(self.parts[STRIP_PART].id())
427 {
428 cx.request_focus(cx.id());
430 cx.request_frame_in(Duration::ZERO);
431 }
432 cx.paint_child(&self.parts[BODY], body_rect);
435 if self.interactive() {
436 let arrow = match (self.side, self.open) {
437 (Side::Left, true) | (Side::Right, false) => "edge-left",
438 (Side::Left, false) | (Side::Right, true) => "edge-right",
439 };
440 let toggle = self.on_toggle.as_ref().map(|_| Toggle { arrow, reach_after: self.side == Side::Left });
442 boundary::paint(cx, self.handle_rect(area, width), toggle);
443 }
444 if progress > 0.0 && progress < 1.0 {
445 cx.request_frame_in(Duration::from_millis(16));
446 }
447 }
448
449 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
450 if let Event::Key(key) = event
451 && self.on_toggle.is_some()
452 && cx.env().keymap().chords_for(Scope::Global, "toggle-panel").contains(&key.chord)
453 {
454 self.toggle(cx);
455 return true;
456 }
457 if !self.interactive() {
458 return false;
459 }
460 let area = cx.area();
461 let current = i32::from(self.open_width(area));
462 let outward = match self.side {
463 Side::Left => 1,
464 Side::Right => -1,
465 };
466 match boundary::event(cx, event, Axis::Columns) {
467 Change::Ignored => false,
468 Change::Used => true,
469 Change::Activate => {
470 self.toggle(cx);
471 true
472 }
473 Change::DragTo(x) => {
474 let target = match self.side {
476 Side::Left => x - area.x + 1,
477 Side::Right => area.right() - x,
478 } - i32::from(self.strip_width());
479 if self.open {
480 self.resize(cx, target);
481 } else {
482 self.drag_open(cx, x, target);
483 }
484 true
485 }
486 Change::Nudge(cells) => {
487 self.resize(cx, current + cells * outward);
488 true
489 }
490 Change::Jump(end) => {
491 let wide = (end && outward > 0) || (!end && outward < 0);
492 self.resize(cx, if wide { i32::MAX } else { 0 });
493 true
494 }
495 }
496 }
497
498 fn focusable(&self) -> bool {
499 self.interactive()
500 }
501
502 fn children(&self) -> &[Node<Msg>] {
503 &self.parts
504 }
505
506 fn children_mut(&mut self) -> &mut [Node<Msg>] {
507 &mut self.parts
508 }
509}