mod strip;
#[cfg(test)]
mod tests;
use std::rc::Rc;
use std::time::Duration;
use crate::event::Event;
use crate::geometry::{Rect, Size};
use crate::keymap::Scope;
use crate::motion::{Easing, steps};
use crate::widget::{Axis as FlexAxis, EventCx, Flex, Length, MeasureCx, Node, NodeMut, PaintCx, View, Widget};
use super::boundary::{self, Axis, Change, Toggle};
use strip::{OpenMessage, STRIP, Strip, ViewMessage};
type Part<'a, Msg> = Box<dyn FnOnce(&mut View<'_, Msg>) + 'a>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Side {
#[default]
Left,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Closed {
#[default]
Collapse,
Hide,
}
pub struct SidePanel<'a, Msg> {
side: Side,
width: u16,
open: bool,
closed: Closed,
min: u16,
max: Option<u16>,
on_toggle: Option<OpenMessage<Msg>>,
on_resize: Option<Box<dyn Fn(u16) -> Msg>>,
strip: Vec<String>,
on_strip: Option<ViewMessage<Msg>>,
active_view: Option<u16>,
panel: Option<Part<'a, Msg>>,
body: Option<Part<'a, Msg>>,
}
impl<'a, Msg: 'static> SidePanel<'a, Msg> {
#[must_use]
pub fn new(width: u16) -> Self {
Self {
side: Side::Left,
width,
open: true,
closed: Closed::Collapse,
min: 8,
max: None,
on_toggle: None,
on_resize: None,
strip: Vec::new(),
on_strip: None,
active_view: None,
panel: None,
body: None,
}
}
#[must_use]
pub fn side(mut self, side: Side) -> Self {
self.side = side;
self
}
#[must_use]
pub fn open(mut self, open: bool) -> Self {
self.open = open;
self
}
#[must_use]
pub fn closed(mut self, closed: Closed) -> Self {
self.closed = closed;
self
}
#[must_use]
pub fn on_toggle(mut self, message: impl Fn(bool) -> Msg + 'static) -> Self {
self.on_toggle = Some(Rc::new(message));
self
}
#[must_use]
pub fn on_resize(mut self, message: impl Fn(u16) -> Msg + 'static) -> Self {
self.on_resize = Some(Box::new(message));
self
}
#[must_use]
pub fn limits(mut self, min: u16, max: impl Into<Option<u16>>) -> Self {
self.min = min;
self.max = max.into();
self
}
#[must_use]
pub fn strip(
mut self,
icons: impl IntoIterator<Item = impl Into<String>>,
message: impl Fn(u16) -> Msg + 'static,
) -> Self {
self.strip = icons.into_iter().map(Into::into).collect();
self.on_strip = Some(Rc::new(message));
self
}
#[must_use]
pub fn active_view(mut self, index: u16) -> Self {
self.active_view = Some(index);
self
}
#[must_use]
pub fn panel(mut self, build: impl FnOnce(&mut View<'_, Msg>) + 'a) -> Self {
self.panel = Some(Box::new(build));
self
}
#[must_use]
pub fn body(mut self, build: impl FnOnce(&mut View<'_, Msg>) + 'a) -> Self {
self.body = Some(Box::new(build));
self
}
pub fn show<'v>(self, ui: &'v mut View<'_, Msg>) -> NodeMut<'v, Msg> {
let build = |part: Option<Part<'a, Msg>>| {
let mut children = Vec::new();
if let Some(part) = part {
part(&mut ui.nested(&mut children));
}
let mut node = Node::new(Flex::new(FlexAxis::Column, children), 0);
node.layout.width = Length::Fill(1);
node.layout.height = Length::Fill(1);
node
};
let mut parts = vec![build(self.panel), build(self.body)];
let has_strip = match self.on_strip {
Some(on_select) if !self.strip.is_empty() => {
let strip = Strip {
side: self.side,
icons: self.strip,
active: self.active_view,
open: self.open,
on_select,
on_toggle: self.on_toggle.clone(),
};
parts.push(Node::new(strip, 2));
true
}
_ => false,
};
let dock = Dock {
side: self.side,
width: self.width,
open: self.open,
closed: self.closed,
min: self.min,
max: self.max,
on_toggle: self.on_toggle,
on_resize: self.on_resize,
has_strip,
parts,
};
ui.add(dock).fill()
}
}
const PANEL: usize = 0;
const BODY: usize = 1;
const STRIP_PART: usize = 2;
struct Dock<Msg> {
side: Side,
width: u16,
open: bool,
closed: Closed,
min: u16,
max: Option<u16>,
on_toggle: Option<OpenMessage<Msg>>,
on_resize: Option<Box<dyn Fn(u16) -> Msg>>,
has_strip: bool,
parts: Vec<Node<Msg>>,
}
impl<Msg: 'static> Dock<Msg> {
fn interactive(&self) -> bool {
self.on_toggle.is_some() || self.on_resize.is_some()
}
fn strip_width(&self) -> u16 {
if self.has_strip { STRIP } else { 0 }
}
fn collapsed_width(&self) -> u16 {
match self.closed {
Closed::Collapse => self.strip_width(),
Closed::Hide => 0,
}
}
fn room(&self, area: Rect) -> u16 {
area.width.saturating_sub(area.width / 4).saturating_sub(self.strip_width())
}
fn max_width(&self, area: Rect) -> u16 {
let room = self.room(area);
self.max.map_or(room, |max| max.min(room))
}
fn open_width(&self, area: Rect) -> u16 {
boundary::clamp_size(i32::from(self.width), self.min, self.max_width(area)).min(self.room(area))
}
fn edge_rect(&self, area: Rect, width: u16) -> Rect {
match self.side {
Side::Left => Rect::new(area.x, area.y, width, area.height),
Side::Right => Rect::new(area.right() - i32::from(width), area.y, width, area.height),
}
}
fn handle_rect(&self, area: Rect, width: u16) -> Rect {
let panel = self.edge_rect(area, width.max(1));
match self.side {
Side::Left => Rect::new(panel.right() - 1, area.y, 1, area.height),
Side::Right => Rect::new(panel.x, area.y, 1, area.height),
}
}
fn toggle(&self, cx: &mut EventCx<'_, Msg>) {
if let Some(message) = &self.on_toggle {
cx.emit(message(!self.open));
}
}
fn resize(&self, cx: &mut EventCx<'_, Msg>, target: i32) {
let area = cx.area();
let width = boundary::clamp_size(target, self.min, self.max_width(area));
if let Some(message) = &self.on_resize
&& self.open
&& width != self.open_width(area)
{
cx.emit(message(width));
}
}
fn drag_open(&self, cx: &mut EventCx<'_, Msg>, x: i32, target: i32) {
let Some(toggle) = &self.on_toggle else { return };
let area = cx.area();
let edge = self.handle_rect(area, self.collapsed_width().min(area.width)).x;
let past_edge = match self.side {
Side::Left => x - edge,
Side::Right => edge - x,
};
if past_edge < i32::from((self.min / 2).max(1)) {
return;
}
cx.emit(toggle(true));
if let Some(message) = &self.on_resize {
let width = boundary::clamp_size(target, self.min, self.max_width(area));
if width != self.open_width(area) {
cx.emit(message(width));
}
}
}
fn paint_sliding(&self, cx: &mut PaintCx<'_>, area: Rect, width: u16, open_width: u16) {
let collapsed = self.collapsed_width();
let visible = self.edge_rect(area, width);
let sliding = width.saturating_sub(collapsed);
let strip = self.strip_width().saturating_sub(collapsed);
let layer_width = strip + open_width;
let (moving, layer_x) = match self.side {
Side::Left => (
Rect::new(visible.x + i32::from(collapsed), area.y, sliding, area.height),
visible.right() - i32::from(layer_width),
),
Side::Right => (Rect::new(visible.x, area.y, sliding, area.height), visible.x),
};
let (strip_x, panel_x) = match self.side {
Side::Left => (layer_x, layer_x + i32::from(strip)),
Side::Right => (layer_x + i32::from(open_width), layer_x),
};
let panel = Rect::new(panel_x, area.y, open_width, area.height);
let background = cx.style("side-panel", None, &[]).text().bg.unwrap_or_else(|| cx.color("surface"));
let handle = u16::from(self.interactive());
let (content, inside) = match self.side {
Side::Left => (
Rect::new(panel.x, area.y, open_width.saturating_sub(handle), area.height),
Rect::new(moving.x, area.y, sliding.saturating_sub(handle), area.height),
),
Side::Right => (
Rect::new(panel.x + i32::from(handle), area.y, open_width.saturating_sub(handle), area.height),
Rect::new(moving.x + i32::from(handle), area.y, sliding.saturating_sub(handle), area.height),
),
};
cx.with_clip(moving, |cx| cx.clear(panel, background));
cx.with_clip(inside, |cx| {
cx.paint_child(&self.parts[PANEL], content);
if strip > 0 {
cx.paint_child(&self.parts[STRIP_PART], Rect::new(strip_x, area.y, strip, area.height));
}
});
}
}
impl<Msg: 'static> Widget<Msg> for Dock<Msg> {
fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
available
}
fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
let open_width = self.open_width(area);
let full = (self.strip_width() + open_width).min(area.width);
let collapsed = self.collapsed_width().min(full);
let duration = cx.env().theme().motion().enter * 2;
let progress = cx.animate("open", if self.open { 1.0 } else { 0.0 }, duration, Easing::EaseOut);
let width = collapsed + steps(progress, full - collapsed);
let gutter = u16::from(self.interactive() && width == 0);
let body_rect = match self.side {
Side::Left => Rect::new(
area.x + i32::from(width + gutter),
area.y,
area.width.saturating_sub(width + gutter),
area.height,
),
Side::Right => Rect::new(area.x, area.y, area.width.saturating_sub(width + gutter), area.height),
};
if collapsed > 0 {
cx.paint_child(&self.parts[STRIP_PART], self.edge_rect(area, collapsed));
}
if width > collapsed {
self.paint_sliding(cx, area, width, open_width);
} else if self.has_strip
&& collapsed == 0
&& self.interactive()
&& cx.focused() == Some(self.parts[STRIP_PART].id())
{
cx.request_focus(cx.id());
cx.request_frame_in(Duration::ZERO);
}
cx.paint_child(&self.parts[BODY], body_rect);
if self.interactive() {
let arrow = match (self.side, self.open) {
(Side::Left, true) | (Side::Right, false) => "edge-left",
(Side::Left, false) | (Side::Right, true) => "edge-right",
};
let toggle = self.on_toggle.as_ref().map(|_| Toggle { arrow, reach_after: self.side == Side::Left });
boundary::paint(cx, self.handle_rect(area, width), toggle);
}
if progress > 0.0 && progress < 1.0 {
cx.request_frame_in(Duration::from_millis(16));
}
}
fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
if let Event::Key(key) = event
&& self.on_toggle.is_some()
&& cx.env().keymap().chords_for(Scope::Global, "toggle-panel").contains(&key.chord)
{
self.toggle(cx);
return true;
}
if !self.interactive() {
return false;
}
let area = cx.area();
let current = i32::from(self.open_width(area));
let outward = match self.side {
Side::Left => 1,
Side::Right => -1,
};
match boundary::event(cx, event, Axis::Columns) {
Change::Ignored => false,
Change::Used => true,
Change::Activate => {
self.toggle(cx);
true
}
Change::DragTo(x) => {
let target = match self.side {
Side::Left => x - area.x + 1,
Side::Right => area.right() - x,
} - i32::from(self.strip_width());
if self.open {
self.resize(cx, target);
} else {
self.drag_open(cx, x, target);
}
true
}
Change::Nudge(cells) => {
self.resize(cx, current + cells * outward);
true
}
Change::Jump(end) => {
let wide = (end && outward > 0) || (!end && outward < 0);
self.resize(cx, if wide { i32::MAX } else { 0 });
true
}
}
}
fn focusable(&self) -> bool {
self.interactive()
}
fn children(&self) -> &[Node<Msg>] {
&self.parts
}
fn children_mut(&mut self) -> &mut [Node<Msg>] {
&mut self.parts
}
}