use iced::widget;
use iced::{Element, Length};
use crate::keys::Keymap;
use crate::layout::resizable_panel::{self, Panel, panel};
pub use crate::layout::resizable_panel::{Action, Axis, Event, Extent, Output, default_keymap};
#[derive(Debug, Clone, PartialEq)]
pub struct State {
group: resizable_panel::State,
}
impl State {
pub fn new(ratio: f32) -> Self {
let ratio = clamp_ratio(ratio);
Self::with_panels(
panel(Extent::Fraction(ratio)),
panel(Extent::Fraction(1.0 - ratio)),
)
}
pub fn with_panels(start: Panel, end: Panel) -> Self {
Self {
group: resizable_panel::State::new([start, end]),
}
}
pub fn min(self, start: Extent, end: Extent) -> Self {
self.map_panels(|[first, second]| [first.min(start), second.min(end)])
}
pub fn max(self, start: Extent, end: Extent) -> Self {
self.map_panels(|[first, second]| [first.max(start), second.max(end)])
}
pub fn collapsible(self, start: bool, end: bool) -> Self {
self.map_panels(|[first, second]| [first.collapsible(start), second.collapsible(end)])
}
fn map_panels(self, change: impl FnOnce([Panel; 2]) -> [Panel; 2]) -> Self {
let panels = self.group.panels();
let (Some(first), Some(second)) = (panels.first(), panels.get(1)) else {
return self;
};
let ratio = self.ratio();
let [start, end] = change([*first, *second]);
let mut state = Self::with_panels(start, end);
if !self.group.sizes().is_empty() {
state.group.set_sizes([ratio, 1.0 - ratio]);
}
state
}
pub fn with_ratio(mut self, ratio: f32) -> Self {
self.set_ratio(ratio);
self
}
pub fn set_ratio(&mut self, ratio: f32) {
let ratio = clamp_ratio(ratio);
self.group.set_sizes([ratio, 1.0 - ratio]);
}
pub fn ratio(&self) -> f32 {
ratio_of(&self.group.sizes())
}
pub fn is_start_collapsed(&self) -> bool {
self.group.is_collapsed(0)
}
pub fn is_end_collapsed(&self) -> bool {
self.group.is_collapsed(1)
}
pub fn group(&self) -> &resizable_panel::State {
&self.group
}
pub fn update(&mut self, event: Event) -> Option<Output> {
self.group.update(event)
}
}
impl Default for State {
fn default() -> Self {
Self::new(0.5)
}
}
fn clamp_ratio(ratio: f32) -> f32 {
if ratio.is_nan() {
return 0.5;
}
ratio.clamp(0.0, 1.0)
}
pub fn ratio_of(sizes: &[f32]) -> f32 {
let total: f32 = sizes.iter().sum();
match sizes.first() {
Some(first) if total > 0.0 => clamp_ratio(first / total),
_ => 0.5,
}
}
pub struct SplitPane<'a, Message> {
state: &'a State,
start: Element<'a, Message>,
end: Element<'a, Message>,
axis: Axis,
stack_below: Option<f32>,
grip: bool,
width: Length,
height: Length,
id: Option<widget::Id>,
keymap: Keymap<Action>,
on_event: Option<Box<dyn Fn(Event) -> Message + 'a>>,
}
impl<Message> std::fmt::Debug for SplitPane<'_, Message> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SplitPane")
.field("state", self.state)
.field("axis", &self.axis)
.field("stack_below", &self.stack_below)
.field("grip", &self.grip)
.finish_non_exhaustive()
}
}
pub fn split_pane<'a, Message>(
state: &'a State,
start: impl Into<Element<'a, Message>>,
end: impl Into<Element<'a, Message>>,
) -> SplitPane<'a, Message> {
SplitPane {
state,
start: start.into(),
end: end.into(),
axis: Axis::default(),
stack_below: None,
grip: true,
width: Length::Fill,
height: Length::Fill,
id: None,
keymap: default_keymap(),
on_event: None,
}
}
impl<'a, Message> SplitPane<'a, Message> {
pub fn axis(mut self, axis: Axis) -> Self {
self.axis = axis;
self
}
pub fn stack_below(mut self, width: f32) -> Self {
self.stack_below = Some(width);
self
}
pub fn grip(mut self, grip: bool) -> Self {
self.grip = grip;
self
}
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
self.id = Some(id.into());
self
}
pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
self.keymap = keymap;
self
}
pub fn on_event(mut self, on_event: impl Fn(Event) -> Message + 'a) -> Self {
self.on_event = Some(Box::new(on_event));
self
}
}
impl<'a, Message: 'a> From<SplitPane<'a, Message>> for Element<'a, Message> {
fn from(split: SplitPane<'a, Message>) -> Self {
let SplitPane {
state,
start,
end,
axis,
stack_below,
grip,
width,
height,
id,
keymap,
on_event,
} = split;
let mut group = resizable_panel::resizable_panel(&state.group, [start, end])
.axis(axis)
.grip(grip)
.width(width)
.height(height)
.keymap(keymap);
if let Some(breakpoint) = stack_below {
group = group.stack_below(breakpoint);
}
if let Some(id) = id {
group = group.id(id);
}
if let Some(on_event) = on_event {
group = group.on_event(on_event);
}
group.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-3
}
fn measured(state: State) -> State {
let mut state = state;
let _ = state.update(Event::Measure(1000.0));
state
}
#[test]
fn ratio_is_clamped_and_defaults_to_even() {
assert!(close(State::new(0.3).ratio(), 0.3));
assert!(close(State::new(1.5).ratio(), 1.0));
assert!(close(State::new(-1.0).ratio(), 0.0));
assert!(close(State::new(f32::NAN).ratio(), 0.5));
assert!(close(State::default().ratio(), 0.5));
}
#[test]
fn ratio_of_sizes() {
assert!(close(ratio_of(&[300.0, 700.0]), 0.3));
assert!(close(ratio_of(&[1.0, 3.0]), 0.25));
assert!(close(ratio_of(&[]), 0.5));
assert!(close(ratio_of(&[0.0, 0.0]), 0.5));
}
#[test]
fn dragging_changes_the_ratio() {
let mut state = measured(State::new(0.5));
let _ = state.update(Event::DragStart {
handle: 0,
position: 500.0,
});
assert_eq!(state.update(Event::Drag(650.0)), Some(Output::Resized));
assert!(close(state.ratio(), 0.65));
}
#[test]
fn limits_hold_the_ratio_in_range() {
let mut state = measured(State::new(0.5).min(Extent::Pixels(200.0), Extent::Fraction(0.4)));
let _ = state.update(Event::Resize {
handle: 0,
delta: 400.0,
});
assert!(close(state.ratio(), 0.6), "end keeps 40%");
let _ = state.update(Event::Resize {
handle: 0,
delta: -900.0,
});
assert!(close(state.ratio(), 0.2), "start keeps 200px");
let capped = measured(State::new(0.9).max(Extent::Fraction(0.7), Extent::Fraction(1.0)));
assert!(close(capped.ratio(), 0.7));
}
#[test]
fn set_ratio_fits_the_limits_once_measured() {
let mut state = measured(State::new(0.5).min(Extent::Pixels(100.0), Extent::Pixels(300.0)));
state.set_ratio(0.9);
assert!(close(state.ratio(), 0.7));
let restored = State::new(0.5).with_ratio(0.25);
assert!(close(restored.ratio(), 0.25));
}
#[test]
fn limits_keep_a_ratio_set_earlier() {
let state = State::new(0.5)
.with_ratio(0.3)
.min(Extent::Pixels(10.0), Extent::Pixels(10.0));
assert!(close(state.ratio(), 0.3));
}
#[test]
fn either_side_can_collapse() {
let mut state = measured(
State::new(0.5)
.min(Extent::Pixels(200.0), Extent::Pixels(200.0))
.collapsible(false, true),
);
assert_eq!(state.update(Event::Toggle(0)), None);
assert_eq!(state.update(Event::Toggle(1)), Some(Output::Collapsed(1)));
assert!(state.is_end_collapsed());
assert!(!state.is_start_collapsed());
assert!(close(state.ratio(), 1.0));
}
#[test]
fn builder_defaults_show_a_grip() {
let state = State::default();
let split: SplitPane<'_, ()> = split_pane(&state, "start", "end");
assert!(split.grip);
assert_eq!(split.axis, Axis::Horizontal);
assert_eq!(split.stack_below, None);
assert!(split.on_event.is_none());
}
}