diff options
author | 2022-03-10 19:25:57 +0700 | |
---|---|---|
committer | 2022-03-14 17:44:25 +0700 | |
commit | 6dd187ff0822230f084e43636b1aabeb1baf06f6 (patch) | |
tree | 27426980b2882025a3c812ba2401d3bf8f6dca8d | |
parent | 9f27969d14232355ad628431fb67aa07e42e768f (diff) | |
download | iced-6dd187ff0822230f084e43636b1aabeb1baf06f6.tar.gz iced-6dd187ff0822230f084e43636b1aabeb1baf06f6.tar.bz2 iced-6dd187ff0822230f084e43636b1aabeb1baf06f6.zip |
Implement `pure` version of `PaneGrid` widget
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | examples/pure/pane_grid/Cargo.toml | 11 | ||||
-rw-r--r-- | examples/pure/pane_grid/src/main.rs | 437 | ||||
-rw-r--r-- | native/src/widget/pane_grid.rs | 781 | ||||
-rw-r--r-- | native/src/widget/pane_grid/axis.rs | 7 | ||||
-rw-r--r-- | native/src/widget/pane_grid/content.rs | 39 | ||||
-rw-r--r-- | native/src/widget/pane_grid/draggable.rs | 12 | ||||
-rw-r--r-- | native/src/widget/pane_grid/state.rs | 114 | ||||
-rw-r--r-- | pure/src/widget.rs | 2 | ||||
-rw-r--r-- | pure/src/widget/pane_grid.rs | 399 | ||||
-rw-r--r-- | pure/src/widget/pane_grid/content.rs | 331 | ||||
-rw-r--r-- | pure/src/widget/pane_grid/title_bar.rs | 355 | ||||
-rw-r--r-- | pure/src/widget/tree.rs | 17 |
13 files changed, 2098 insertions, 408 deletions
@@ -89,6 +89,7 @@ members = [ "examples/pure/component", "examples/pure/counter", "examples/pure/game_of_life", + "examples/pure/pane_grid", "examples/pure/pick_list", "examples/pure/todos", "examples/pure/tour", diff --git a/examples/pure/pane_grid/Cargo.toml b/examples/pure/pane_grid/Cargo.toml new file mode 100644 index 00000000..a51cdaf0 --- /dev/null +++ b/examples/pure/pane_grid/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "pure_pane_grid" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["pure", "debug"] } +iced_native = { path = "../../../native" } +iced_lazy = { path = "../../../lazy", features = ["pure"] } diff --git a/examples/pure/pane_grid/src/main.rs b/examples/pure/pane_grid/src/main.rs new file mode 100644 index 00000000..6232d6be --- /dev/null +++ b/examples/pure/pane_grid/src/main.rs @@ -0,0 +1,437 @@ +use iced::alignment::{self, Alignment}; +use iced::executor; +use iced::keyboard; +use iced::pure::pane_grid::{self, PaneGrid}; +use iced::pure::{button, column, container, row, scrollable, text}; +use iced::pure::{Application, Element}; +use iced::{Color, Command, Length, Settings, Size, Subscription}; +use iced_lazy::pure::responsive; +use iced_native::{event, subscription, Event}; + +pub fn main() -> iced::Result { + Example::run(Settings::default()) +} + +struct Example { + panes: pane_grid::Map<Pane>, + panes_created: usize, + focus: Option<pane_grid::Pane>, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Split(pane_grid::Axis, pane_grid::Pane), + SplitFocused(pane_grid::Axis), + FocusAdjacent(pane_grid::Direction), + Clicked(pane_grid::Pane), + Dragged(pane_grid::DragEvent), + Resized(pane_grid::ResizeEvent), + TogglePin(pane_grid::Pane), + Close(pane_grid::Pane), + CloseFocused, +} + +impl Application for Example { + type Message = Message; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command<Message>) { + let (panes, _) = pane_grid::Map::new(Pane::new(0)); + + ( + Example { + panes, + panes_created: 1, + focus: None, + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Pane grid - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Split(axis, pane) => { + let result = self.panes.split( + axis, + &pane, + Pane::new(self.panes_created), + ); + + if let Some((pane, _)) = result { + self.focus = Some(pane); + } + + self.panes_created += 1; + } + Message::SplitFocused(axis) => { + if let Some(pane) = self.focus { + let result = self.panes.split( + axis, + &pane, + Pane::new(self.panes_created), + ); + + if let Some((pane, _)) = result { + self.focus = Some(pane); + } + + self.panes_created += 1; + } + } + Message::FocusAdjacent(direction) => { + if let Some(pane) = self.focus { + if let Some(adjacent) = + self.panes.adjacent(&pane, direction) + { + self.focus = Some(adjacent); + } + } + } + Message::Clicked(pane) => { + self.focus = Some(pane); + } + Message::Resized(pane_grid::ResizeEvent { split, ratio }) => { + self.panes.resize(&split, ratio); + } + Message::Dragged(pane_grid::DragEvent::Dropped { + pane, + target, + }) => { + self.panes.swap(&pane, &target); + } + Message::Dragged(_) => {} + Message::TogglePin(pane) => { + if let Some(Pane { is_pinned, .. }) = self.panes.get_mut(&pane) + { + *is_pinned = !*is_pinned; + } + } + Message::Close(pane) => { + if let Some((_, sibling)) = self.panes.close(&pane) { + self.focus = Some(sibling); + } + } + Message::CloseFocused => { + if let Some(pane) = self.focus { + if let Some(Pane { is_pinned, .. }) = self.panes.get(&pane) + { + if !is_pinned { + if let Some((_, sibling)) = self.panes.close(&pane) + { + self.focus = Some(sibling); + } + } + } + } + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + subscription::events_with(|event, status| { + if let event::Status::Captured = status { + return None; + } + + match event { + Event::Keyboard(keyboard::Event::KeyPressed { + modifiers, + key_code, + }) if modifiers.command() => handle_hotkey(key_code), + _ => None, + } + }) + } + + fn view(&self) -> Element<Message> { + let focus = self.focus; + let total_panes = self.panes.len(); + + let pane_grid = PaneGrid::new(&mut self.panes, |id, pane| { + let is_focused = focus == Some(id); + + let Pane { id, is_pinned } = pane; + + let pin_button = + button(text(if *is_pinned { "Unpin" } else { "Pin" }).size(14)) + .on_press(Message::TogglePin(id)) + .style(style::Button::Pin) + .padding(3); + + let title = row() + .push(pin_button) + .push("Pane") + .push(text(id.to_string()).color(if is_focused { + PANE_ID_COLOR_FOCUSED + } else { + PANE_ID_COLOR_UNFOCUSED + })) + .spacing(5); + + let title_bar = pane_grid::TitleBar::new(title) + .controls(view_controls(id, total_panes, *is_pinned)) + .padding(10) + .style(if is_focused { + style::TitleBar::Focused + } else { + style::TitleBar::Active + }); + + pane_grid::Content::new(responsive(move |size| { + view_content(id, total_panes, *is_pinned, size) + })) + .title_bar(title_bar) + .style(if is_focused { + style::Pane::Focused + } else { + style::Pane::Active + }) + }) + .width(Length::Fill) + .height(Length::Fill) + .spacing(10) + .on_click(Message::Clicked) + .on_drag(Message::Dragged) + .on_resize(10, Message::Resized); + + container(pane_grid) + .width(Length::Fill) + .height(Length::Fill) + .padding(10) + .into() + } +} + +const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb( + 0xFF as f32 / 255.0, + 0xC7 as f32 / 255.0, + 0xC7 as f32 / 255.0, +); +const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb( + 0xFF as f32 / 255.0, + 0x47 as f32 / 255.0, + 0x47 as f32 / 255.0, +); + +fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> { + use keyboard::KeyCode; + use pane_grid::{Axis, Direction}; + + let direction = match key_code { + KeyCode::Up => Some(Direction::Up), + KeyCode::Down => Some(Direction::Down), + KeyCode::Left => Some(Direction::Left), + KeyCode::Right => Some(Direction::Right), + _ => None, + }; + + match key_code { + KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)), + KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)), + KeyCode::W => Some(Message::CloseFocused), + _ => direction.map(Message::FocusAdjacent), + } +} + +struct Pane { + id: usize, + pub is_pinned: bool, +} + +impl Pane { + fn new(id: usize) -> Self { + Self { + id, + is_pinned: false, + } + } +} + +fn view_content<'a>( + pane: pane_grid::Pane, + total_panes: usize, + is_pinned: bool, + size: Size, +) -> Element<'a, Message> { + let button = |label, message, style| { + button( + text(label) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center) + .size(16), + ) + .width(Length::Fill) + .padding(8) + .on_press(message) + .style(style) + }; + + let mut controls = column() + .spacing(5) + .max_width(150) + .push(button( + "Split horizontally", + Message::Split(pane_grid::Axis::Horizontal, pane), + style::Button::Primary, + )) + .push(button( + "Split vertically", + Message::Split(pane_grid::Axis::Vertical, pane), + style::Button::Primary, + )); + + if total_panes > 1 && !is_pinned { + controls = controls.push(button( + "Close", + Message::Close(pane), + style::Button::Destructive, + )); + } + + let content = column() + .width(Length::Fill) + .spacing(10) + .align_items(Alignment::Center) + .push(text(format!("{}x{}", size.width, size.height)).size(24)) + .push(controls); + + container(scrollable(content)) + .width(Length::Fill) + .height(Length::Fill) + .padding(5) + .center_y() + .into() +} + +fn view_controls<'a>( + pane: pane_grid::Pane, + total_panes: usize, + is_pinned: bool, +) -> Element<'a, Message> { + let mut button = button(text("Close").size(14)) + .style(style::Button::Control) + .padding(3); + + if total_panes > 1 && !is_pinned { + button = button.on_press(Message::Close(pane)); + } + + button.into() +} + +mod style { + use crate::PANE_ID_COLOR_FOCUSED; + use iced::{button, container, Background, Color, Vector}; + + const SURFACE: Color = Color::from_rgb( + 0xF2 as f32 / 255.0, + 0xF3 as f32 / 255.0, + 0xF5 as f32 / 255.0, + ); + + const ACTIVE: Color = Color::from_rgb( + 0x72 as f32 / 255.0, + 0x89 as f32 / 255.0, + 0xDA as f32 / 255.0, + ); + + const HOVERED: Color = Color::from_rgb( + 0x67 as f32 / 255.0, + 0x7B as f32 / 255.0, + 0xC4 as f32 / 255.0, + ); + + pub enum TitleBar { + Active, + Focused, + } + + impl container::StyleSheet for TitleBar { + fn style(&self) -> container::Style { + let pane = match self { + Self::Active => Pane::Active, + Self::Focused => Pane::Focused, + } + .style(); + + container::Style { + text_color: Some(Color::WHITE), + background: Some(pane.border_color.into()), + ..Default::default() + } + } + } + + pub enum Pane { + Active, + Focused, + } + + impl container::StyleSheet for Pane { + fn style(&self) -> container::Style { + container::Style { + background: Some(Background::Color(SURFACE)), + border_width: 2.0, + border_color: match self { + Self::Active => Color::from_rgb(0.7, 0.7, 0.7), + Self::Focused => Color::BLACK, + }, + ..Default::default() + } + } + } + + pub enum Button { + Primary, + Destructive, + Control, + Pin, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + let (background, text_color) = match self { + Button::Primary => (Some(ACTIVE), Color::WHITE), + Button::Destructive => { + (None, Color::from_rgb8(0xFF, 0x47, 0x47)) + } + Button::Control => (Some(PANE_ID_COLOR_FOCUSED), Color::WHITE), + Button::Pin => (Some(ACTIVE), Color::WHITE), + }; + + button::Style { + text_color, + background: background.map(Background::Color), + border_radius: 5.0, + shadow_offset: Vector::new(0.0, 0.0), + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + let active = self.active(); + + let background = match self { + Button::Primary => Some(HOVERED), + Button::Destructive => Some(Color { + a: 0.2, + ..active.text_color + }), + Button::Control => Some(PANE_ID_COLOR_FOCUSED), + Button::Pin => Some(HOVERED), + }; + + button::Style { + background: background.map(Background::Color), + ..active + } + } + } +} diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 8ad63cf1..2093886e 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -11,16 +11,19 @@ mod axis; mod configuration; mod content; mod direction; +mod draggable; mod node; mod pane; mod split; -mod state; mod title_bar; +pub mod state; + pub use axis::Axis; pub use configuration::Configuration; pub use content::Content; pub use direction::Direction; +pub use draggable::Draggable; pub use node::Node; pub use pane::Pane; pub use split::Split; @@ -92,6 +95,7 @@ pub use iced_style::pane_grid::{Line, StyleSheet}; #[allow(missing_debug_implementations)] pub struct PaneGrid<'a, Message, Renderer> { state: &'a mut state::Internal, + action: &'a mut state::Action, elements: Vec<(Pane, Content<'a, Message, Renderer>)>, width: Length, height: Length, @@ -124,6 +128,7 @@ where Self { state: &mut state.internal, + action: &mut state.action, elements, width: Length::Fill, height: Length::Fill, @@ -197,80 +202,407 @@ where } } -impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn click_pane( - &mut self, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - ) { - let mut clicked_region = - self.elements.iter().zip(layout.children()).filter( - |(_, layout)| layout.bounds().contains(cursor_position), +/// Calculates the [`Layout`] of a [`PaneGrid`]. +pub fn layout<Renderer, T>( + renderer: &Renderer, + limits: &layout::Limits, + state: &state::Internal, + width: Length, + height: Length, + spacing: u16, + elements: impl Iterator<Item = (Pane, T)>, + layout_element: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.width(width).height(height); + let size = limits.resolve(Size::ZERO); + + let regions = state.pane_regions(f32::from(spacing), size); + let children = elements + .filter_map(|(pane, element)| { + let region = regions.get(&pane)?; + let size = Size::new(region.width, region.height); + + let mut node = layout_element( + element, + renderer, + &layout::Limits::new(size, size), ); - if let Some(((pane, content), layout)) = clicked_region.next() { - if let Some(on_click) = &self.on_click { - shell.publish(on_click(*pane)); + node.move_to(Point::new(region.x, region.y)); + + Some(node) + }) + .collect(); + + layout::Node::with_children(size, children) +} + +/// Processes an [`Event`] and updates the [`state`] of a [`PaneGrid`] +/// accordingly. +pub fn update<'a, Message, T: Draggable>( + action: &mut state::Action, + state: &state::Internal, + event: &Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + spacing: u16, + elements: impl Iterator<Item = (Pane, T)>, + on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>, + on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, + on_resize: &Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, +) -> event::Status { + let mut event_status = event::Status::Ignored; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + event_status = event::Status::Captured; + + match on_resize { + Some((leeway, _)) => { + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = state.split_regions( + f32::from(spacing), + Size::new(bounds.width, bounds.height), + ); + + let clicked_split = hovered_split( + splits.iter(), + f32::from(spacing + leeway), + relative_cursor, + ); + + if let Some((split, axis, _)) = clicked_split { + if action.picked_pane().is_none() { + *action = + state::Action::Resizing { split, axis }; + } + } else { + click_pane( + action, + layout, + cursor_position, + shell, + elements, + on_click, + on_drag, + ); + } + } + None => { + click_pane( + action, + layout, + cursor_position, + shell, + elements, + on_click, + on_drag, + ); + } + } } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if let Some((pane, _)) = action.picked_pane() { + if let Some(on_drag) = on_drag { + let mut dropped_region = elements + .zip(layout.children()) + .filter(|(_, layout)| { + layout.bounds().contains(cursor_position) + }); + + let event = match dropped_region.next() { + Some(((target, _), _)) if pane != target => { + DragEvent::Dropped { pane, target } + } + _ => DragEvent::Canceled { pane }, + }; + + shell.publish(on_drag(event)); + } + + *action = state::Action::Idle; + + event_status = event::Status::Captured; + } else if action.picked_split().is_some() { + *action = state::Action::Idle; + + event_status = event::Status::Captured; + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some((_, on_resize)) = on_resize { + if let Some((split, _)) = action.picked_split() { + let bounds = layout.bounds(); + + let splits = state.split_regions( + f32::from(spacing), + Size::new(bounds.width, bounds.height), + ); - if let Some(on_drag) = &self.on_drag { - if content.can_be_picked_at(layout, cursor_position) { - let pane_position = layout.position(); + if let Some((axis, rectangle, _)) = splits.get(&split) { + let ratio = match axis { + Axis::Horizontal => { + let position = + cursor_position.y - bounds.y - rectangle.y; - let origin = cursor_position - - Vector::new(pane_position.x, pane_position.y); + (position / rectangle.height).max(0.1).min(0.9) + } + Axis::Vertical => { + let position = + cursor_position.x - bounds.x - rectangle.x; - self.state.pick_pane(pane, origin); + (position / rectangle.width).max(0.1).min(0.9) + } + }; - shell.publish(on_drag(DragEvent::Picked { pane: *pane })); + shell.publish(on_resize(ResizeEvent { split, ratio })); + + event_status = event::Status::Captured; + } } } } + _ => {} } - fn trigger_resize( - &mut self, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - if let Some((_, on_resize)) = &self.on_resize { - if let Some((split, _)) = self.state.picked_split() { + event_status +} + +fn click_pane<'a, Message, T>( + action: &mut state::Action, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + elements: impl Iterator<Item = (Pane, T)>, + on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>, + on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, +) where + T: Draggable, +{ + let mut clicked_region = elements + .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().contains(cursor_position)); + + if let Some(((pane, content), layout)) = clicked_region.next() { + if let Some(on_click) = &on_click { + shell.publish(on_click(pane)); + } + + if let Some(on_drag) = &on_drag { + if content.can_be_dragged_at(layout, cursor_position) { + let pane_position = layout.position(); + + let origin = cursor_position + - Vector::new(pane_position.x, pane_position.y); + + *action = state::Action::Dragging { pane, origin }; + + shell.publish(on_drag(DragEvent::Picked { pane })); + } + } + } +} + +/// Returns the current [`mouse::Interaction`] of a [`PaneGrid`]. +pub fn mouse_interaction( + action: &state::Action, + state: &state::Internal, + layout: Layout<'_>, + cursor_position: Point, + spacing: u16, + resize_leeway: Option<u16>, +) -> Option<mouse::Interaction> { + if action.picked_pane().is_some() { + return Some(mouse::Interaction::Grab); + } + + let resize_axis = + action.picked_split().map(|(_, axis)| axis).or_else(|| { + resize_leeway.and_then(|leeway| { let bounds = layout.bounds(); - let splits = self.state.split_regions( - f32::from(self.spacing), - Size::new(bounds.width, bounds.height), + let splits = + state.split_regions(f32::from(spacing), bounds.size()); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, ); - if let Some((axis, rectangle, _)) = splits.get(&split) { - let ratio = match axis { - Axis::Horizontal => { - let position = - cursor_position.y - bounds.y - rectangle.y; + hovered_split( + splits.iter(), + f32::from(spacing + leeway), + relative_cursor, + ) + .map(|(_, axis, _)| axis) + }) + }); - (position / rectangle.height).max(0.1).min(0.9) - } - Axis::Vertical => { - let position = - cursor_position.x - bounds.x - rectangle.x; + if let Some(resize_axis) = resize_axis { + return Some(match resize_axis { + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, + }); + } - (position / rectangle.width).max(0.1).min(0.9) - } - }; + None +} + +/// Draws a [`PaneGrid`]. +pub fn draw<Renderer, T>( + action: &state::Action, + state: &state::Internal, + layout: Layout<'_>, + cursor_position: Point, + renderer: &mut Renderer, + style: &renderer::Style, + viewport: &Rectangle, + spacing: u16, + resize_leeway: Option<u16>, + style_sheet: &dyn StyleSheet, + elements: impl Iterator<Item = (Pane, T)>, + draw_pane: impl Fn( + T, + &mut Renderer, + &renderer::Style, + Layout<'_>, + Point, + &Rectangle, + ), +) where + Renderer: crate::Renderer, +{ + let picked_pane = action.picked_pane(); - shell.publish(on_resize(ResizeEvent { split, ratio })); + let picked_split = action + .picked_split() + .and_then(|(split, axis)| { + let bounds = layout.bounds(); - return event::Status::Captured; - } + let splits = state.split_regions(f32::from(spacing), bounds.size()); + + let (_axis, region, ratio) = splits.get(&split)?; + + let region = + axis.split_line_bounds(*region, *ratio, f32::from(spacing)); + + Some((axis, region + Vector::new(bounds.x, bounds.y), true)) + }) + .or_else(|| match resize_leeway { + Some(leeway) => { + let bounds = layout.bounds(); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = + state.split_regions(f32::from(spacing), bounds.size()); + + let (_split, axis, region) = hovered_split( + splits.iter(), + f32::from(spacing + leeway), + relative_cursor, + )?; + + Some((axis, region + Vector::new(bounds.x, bounds.y), false)) + } + None => None, + }); + + let pane_cursor_position = if picked_pane.is_some() { + // TODO: Remove once cursor availability is encoded in the type + // system + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + for ((id, pane), layout) in elements.zip(layout.children()) { + match picked_pane { + Some((dragging, origin)) if id == dragging => { + let bounds = layout.bounds(); + + renderer.with_translation( + cursor_position + - Point::new(bounds.x + origin.x, bounds.y + origin.y), + |renderer| { + renderer.with_layer(bounds, |renderer| { + draw_pane( + pane, + renderer, + style, + layout, + pane_cursor_position, + viewport, + ); + }); + }, + ); + } + _ => { + draw_pane( + pane, + renderer, + style, + layout, + pane_cursor_position, + viewport, + ); } } + } - event::Status::Ignored + if let Some((axis, split_region, is_picked)) = picked_split { + let highlight = if is_picked { + style_sheet.picked_split() + } else { + style_sheet.hovered_split() + }; + + if let Some(highlight) = highlight { + renderer.fill_quad( + renderer::Quad { + bounds: match axis { + Axis::Horizontal => Rectangle { + x: split_region.x, + y: (split_region.y + + (split_region.height - highlight.width) + / 2.0) + .round(), + width: split_region.width, + height: highlight.width, + }, + Axis::Vertical => Rectangle { + x: (split_region.x + + (split_region.width - highlight.width) / 2.0) + .round(), + y: split_region.y, + width: highlight.width, + height: split_region.height, + }, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + highlight.color, + ); + } } } @@ -331,28 +663,16 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - let regions = self.state.pane_regions(f32::from(self.spacing), size); - - let children = self - .elements - .iter() - .filter_map(|(pane, element)| { - let region = regions.get(pane)?; - let size = Size::new(region.width, region.height); - - let mut node = - element.layout(renderer, &layout::Limits::new(size, size)); - - node.move_to(Point::new(region.x, region.y)); - - Some(node) - }) - .collect(); - - layout::Node::with_children(size, children) + layout( + renderer, + limits, + self.state, + self.width, + self.height, + self.spacing, + self.elements.iter().map(|(pane, content)| (*pane, content)), + |element, renderer, limits| element.layout(renderer, limits), + ) } fn on_event( @@ -364,89 +684,21 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - let mut event_status = event::Status::Ignored; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - event_status = event::Status::Captured; - - match self.on_resize { - Some((leeway, _)) => { - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = self.state.split_regions( - f32::from(self.spacing), - Size::new(bounds.width, bounds.height), - ); - - let clicked_split = hovered_split( - splits.iter(), - f32::from(self.spacing + leeway), - relative_cursor, - ); - - if let Some((split, axis, _)) = clicked_split { - self.state.pick_split(&split, axis); - } else { - self.click_pane(layout, cursor_position, shell); - } - } - None => { - self.click_pane(layout, cursor_position, shell); - } - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if let Some((pane, _)) = self.state.picked_pane() { - if let Some(on_drag) = &self.on_drag { - let mut dropped_region = - self.elements.iter().zip(layout.children()).filter( - |(_, layout)| { - layout.bounds().contains(cursor_position) - }, - ); - - let event = match dropped_region.next() { - Some(((target, _), _)) if pane != *target => { - DragEvent::Dropped { - pane, - target: *target, - } - } - _ => DragEvent::Canceled { pane }, - }; - - shell.publish(on_drag(event)); - } - - self.state.idle(); - - event_status = event::Status::Captured; - } else if self.state.picked_split().is_some() { - self.state.idle(); - - event_status = event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - event_status = - self.trigger_resize(layout, cursor_position, shell); - } - _ => {} - } - - let picked_pane = self.state.picked_pane().map(|(pane, _)| pane); + let event_status = update( + self.action, + self.state, + &event, + layout, + cursor_position, + shell, + self.spacing, + self.elements.iter().map(|(pane, content)| (*pane, content)), + &self.on_click, + &self.on_drag, + &self.on_resize, + ); + + let picked_pane = self.action.picked_pane().map(|(pane, _)| pane); self.elements .iter_mut() @@ -474,53 +726,29 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - if self.state.picked_pane().is_some() { - return mouse::Interaction::Grab; - } - - let resize_axis = - self.state.picked_split().map(|(_, axis)| axis).or_else(|| { - self.on_resize.as_ref().and_then(|(leeway, _)| { - let bounds = layout.bounds(); - - let splits = self - .state - .split_regions(f32::from(self.spacing), bounds.size()); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - hovered_split( - splits.iter(), - f32::from(self.spacing + leeway), - relative_cursor, + mouse_interaction( + self.action, + self.state, + layout, + cursor_position, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + ) + .unwrap_or_else(|| { + self.elements + .iter() + .zip(layout.children()) + .map(|((_pane, content), layout)| { + content.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, ) - .map(|(_, axis, _)| axis) }) - }); - - if let Some(resize_axis) = resize_axis { - return match resize_axis { - Axis::Horizontal => mouse::Interaction::ResizingVertically, - Axis::Vertical => mouse::Interaction::ResizingHorizontally, - }; - } - - self.elements - .iter() - .zip(layout.children()) - .map(|((_pane, content), layout)| { - content.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() + .max() + .unwrap_or_default() + }) } fn draw( @@ -531,139 +759,22 @@ where cursor_position: Point, viewport: &Rectangle, ) { - let picked_pane = self.state.picked_pane(); - - let picked_split = self - .state - .picked_split() - .and_then(|(split, axis)| { - let bounds = layout.bounds(); - - let splits = self - .state - .split_regions(f32::from(self.spacing), bounds.size()); - - let (_axis, region, ratio) = splits.get(&split)?; - - let region = axis.split_line_bounds( - *region, - *ratio, - f32::from(self.spacing), - ); - - Some((axis, region + Vector::new(bounds.x, bounds.y), true)) - }) - .or_else(|| match self.on_resize { - Some((leeway, _)) => { - let bounds = layout.bounds(); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = self - .state - .split_regions(f32::from(self.spacing), bounds.size()); - - let (_split, axis, region) = hovered_split( - splits.iter(), - f32::from(self.spacing + leeway), - relative_cursor, - )?; - - Some(( - axis, - region + Vector::new(bounds.x, bounds.y), - false, - )) - } - None => None, - }); - - let pane_cursor_position = if picked_pane.is_some() { - // TODO: Remove once cursor availability is encoded in the type - // system - Point::new(-1.0, -1.0) - } else { - cursor_position - }; - - for ((id, pane), layout) in self.elements.iter().zip(layout.children()) - { - match picked_pane { - Some((dragging, origin)) if *id == dragging => { - let bounds = layout.bounds(); - - renderer.with_translation( - cursor_position - - Point::new( - bounds.x + origin.x, - bounds.y + origin.y, - ), - |renderer| { - renderer.with_layer(bounds, |renderer| { - pane.draw( - renderer, - style, - layout, - pane_cursor_position, - viewport, - ); - }); - }, - ); - } - _ => { - pane.draw( - renderer, - style, - layout, - pane_cursor_position, - viewport, - ); - } - } - } - - if let Some((axis, split_region, is_picked)) = picked_split { - let highlight = if is_picked { - self.style_sheet.picked_split() - } else { - self.style_sheet.hovered_split() - }; - - if let Some(highlight) = highlight { - renderer.fill_quad( - renderer::Quad { - bounds: match axis { - Axis::Horizontal => Rectangle { - x: split_region.x, - y: (split_region.y - + (split_region.height - highlight.width) - / 2.0) - .round(), - width: split_region.width, - height: highlight.width, - }, - Axis::Vertical => Rectangle { - x: (split_region.x - + (split_region.width - highlight.width) - / 2.0) - .round(), - y: split_region.y, - width: highlight.width, - height: split_region.height, - }, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - highlight.color, - ); - } - } + draw( + self.action, + self.state, + layout, + cursor_position, + renderer, + style, + viewport, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + self.style_sheet.as_ref(), + self.elements.iter().map(|(pane, content)| (*pane, content)), + |pane, renderer, style, layout, cursor_position, rectangle| { + pane.draw(renderer, style, layout, cursor_position, rectangle); + }, + ) } fn overlay( diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs index 2320cb7c..02bde064 100644 --- a/native/src/widget/pane_grid/axis.rs +++ b/native/src/widget/pane_grid/axis.rs @@ -10,7 +10,9 @@ pub enum Axis { } impl Axis { - pub(super) fn split( + /// Splits the provided [`Rectangle`] on the current [`Axis`] with the + /// given `ratio` and `spacing`. + pub fn split( &self, rectangle: &Rectangle, ratio: f32, @@ -54,7 +56,8 @@ impl Axis { } } - pub(super) fn split_line_bounds( + /// Calculates the bounds of the split line in a [`Rectangle`] region. + pub fn split_line_bounds( &self, rectangle: Rectangle, ratio: f32, diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 8b0e8d2a..f0ed0426 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -4,7 +4,7 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget::container; -use crate::widget::pane_grid::TitleBar; +use crate::widget::pane_grid::{Draggable, TitleBar}; use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; /// The content of a [`Pane`]. @@ -101,23 +101,6 @@ where } } - /// Returns whether the [`Content`] with the given [`Layout`] can be picked - /// at the provided cursor position. - pub fn can_be_picked_at( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool { - if let Some(title_bar) = &self.title_bar { - let mut children = layout.children(); - let title_bar_layout = children.next().unwrap(); - - title_bar.is_over_pick_area(title_bar_layout, cursor_position) - } else { - false - } - } - pub(crate) fn layout( &self, renderer: &Renderer, @@ -253,6 +236,26 @@ where } } +impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn can_be_dragged_at( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + title_bar.is_over_pick_area(title_bar_layout, cursor_position) + } else { + false + } + } +} + impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer> where T: Into<Element<'a, Message, Renderer>>, diff --git a/native/src/widget/pane_grid/draggable.rs b/native/src/widget/pane_grid/draggable.rs new file mode 100644 index 00000000..6044871d --- /dev/null +++ b/native/src/widget/pane_grid/draggable.rs @@ -0,0 +1,12 @@ +use crate::{Layout, Point}; + +/// A pane that can be dragged. +pub trait Draggable { + /// Returns whether the [`Draggable`] with the given [`Layout`] can be picked + /// at the provided cursor position. + fn can_be_dragged_at( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool; +} diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index feea0dec..f9ea21f4 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -1,3 +1,4 @@ +//! The state of a [`PaneGrid`]. use crate::widget::pane_grid::{ Axis, Configuration, Direction, Node, Pane, Split, }; @@ -19,8 +20,13 @@ use std::collections::{BTreeMap, HashMap}; /// [`PaneGrid::new`]: crate::widget::PaneGrid::new #[derive(Debug, Clone)] pub struct State<T> { - pub(super) panes: HashMap<Pane, T>, - pub(super) internal: Internal, + /// The panes of the [`PaneGrid`]. + pub panes: HashMap<Pane, T>, + + /// The internal state of the [`PaneGrid`]. + pub internal: Internal, + + pub(super) action: Action, } impl<T> State<T> { @@ -39,16 +45,13 @@ impl<T> State<T> { pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self { let mut panes = HashMap::new(); - let (layout, last_id) = - Self::distribute_content(&mut panes, config.into(), 0); + let internal = + Internal::from_configuration(&mut panes, config.into(), 0); State { panes, - internal: Internal { - layout, - last_id, - action: Action::Idle, - }, + internal, + action: Action::Idle, } } @@ -192,16 +195,34 @@ impl<T> State<T> { None } } +} + +/// The internal state of a [`PaneGrid`]. +#[derive(Debug, Clone)] +pub struct Internal { + layout: Node, + last_id: usize, +} - fn distribute_content( +impl Internal { + /// Initializes the [`Internal`] state of a [`PaneGrid`] from a + /// [`Configuration`]. + pub fn from_configuration<T>( panes: &mut HashMap<Pane, T>, content: Configuration<T>, next_id: usize, - ) -> (Node, usize) { - match content { + ) -> Self { + let (layout, last_id) = match content { Configuration::Split { axis, ratio, a, b } => { - let (a, next_id) = Self::distribute_content(panes, *a, next_id); - let (b, next_id) = Self::distribute_content(panes, *b, next_id); + let Internal { + layout: a, + last_id: next_id, + } = Self::from_configuration(panes, *a, next_id); + + let Internal { + layout: b, + last_id: next_id, + } = Self::from_configuration(panes, *b, next_id); ( Node::Split { @@ -220,39 +241,53 @@ impl<T> State<T> { (Node::Pane(id), next_id + 1) } - } - } -} + }; -#[derive(Debug, Clone)] -pub struct Internal { - layout: Node, - last_id: usize, - action: Action, + Self { layout, last_id } + } } +/// The current action of a [`PaneGrid`]. #[derive(Debug, Clone, Copy, PartialEq)] pub enum Action { + /// The [`PaneGrid`] is idle. Idle, - Dragging { pane: Pane, origin: Point }, - Resizing { split: Split, axis: Axis }, + /// A [`Pane`] in the [`PaneGrid`] is being dragged. + Dragging { + /// The [`Pane`] being dragged. + pane: Pane, + /// The starting [`Point`] of the drag interaction. + origin: Point, + }, + /// A [`Split`] in the [`PaneGrid`] is being dragged. + Resizing { + /// The [`Split`] being dragged. + split: Split, + /// The [`Axis`] of the [`Split`]. + axis: Axis, + }, } -impl Internal { +impl Action { + /// Returns the current [`Pane`] that is being dragged, if any. pub fn picked_pane(&self) -> Option<(Pane, Point)> { - match self.action { + match *self { Action::Dragging { pane, origin, .. } => Some((pane, origin)), _ => None, } } + /// Returns the current [`Split`] that is being dragged, if any. pub fn picked_split(&self) -> Option<(Split, Axis)> { - match self.action { + match *self { Action::Resizing { split, axis, .. } => Some((split, axis)), _ => None, } } +} +impl Internal { + /// Calculates the current [`Pane`] regions from the [`PaneGrid`] layout. pub fn pane_regions( &self, spacing: f32, @@ -261,6 +296,7 @@ impl Internal { self.layout.pane_regions(spacing, size) } + /// Calculates the current [`Split`] regions from the [`PaneGrid`] layout. pub fn split_regions( &self, spacing: f32, @@ -268,28 +304,4 @@ impl Internal { ) -> BTreeMap<Split, (Axis, Rectangle, f32)> { self.layout.split_regions(spacing, size) } - - pub fn pick_pane(&mut self, pane: &Pane, origin: Point) { - self.action = Action::Dragging { - pane: *pane, - origin, - }; - } - - pub fn pick_split(&mut self, split: &Split, axis: Axis) { - // TODO: Obtain `axis` from layout itself. Maybe we should implement - // `Node::find_split` - if self.picked_pane().is_some() { - return; - } - - self.action = Action::Resizing { - split: *split, - axis, - }; - } - - pub fn idle(&mut self) { - self.action = Action::Idle; - } } diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 8f2cf920..564f0583 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -1,4 +1,5 @@ pub mod image; +pub mod pane_grid; pub mod progress_bar; pub mod rule; pub mod tree; @@ -24,6 +25,7 @@ pub use column::Column; pub use container::Container; pub use element::Element; pub use image::Image; +pub use pane_grid::PaneGrid; pub use pick_list::PickList; pub use progress_bar::ProgressBar; pub use radio::Radio; diff --git a/pure/src/widget/pane_grid.rs b/pure/src/widget/pane_grid.rs new file mode 100644 index 00000000..717c9ceb --- /dev/null +++ b/pure/src/widget/pane_grid.rs @@ -0,0 +1,399 @@ +//! Let your users split regions of your application and organize layout dynamically. +//! +//! [](https://gfycat.com/mixedflatjellyfish) +//! +//! # Example +//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, +//! drag and drop, and hotkey support. +//! +//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.3/examples/pane_grid +mod content; +mod title_bar; + +pub use content::Content; +pub use title_bar::TitleBar; + +pub use iced_native::widget::pane_grid::{ + Axis, Configuration, Direction, DragEvent, Node, Pane, ResizeEvent, Split, + State, +}; + +use crate::overlay; +use crate::widget::tree::{self, Tree}; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::pane_grid; +use iced_native::widget::pane_grid::state; +use iced_native::{Clipboard, Layout, Length, Point, Rectangle, Shell}; + +pub use iced_style::pane_grid::{Line, StyleSheet}; + +/// A collection of panes distributed using either vertical or horizontal splits +/// to completely fill the space available. +/// +/// [](https://gfycat.com/frailfreshairedaleterrier) +/// +/// This distribution of space is common in tiling window managers (like +/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even +/// [`tmux`](https://github.com/tmux/tmux)). +/// +/// A [`PaneGrid`] supports: +/// +/// * Vertical and horizontal splits +/// * Tracking of the last active pane +/// * Mouse-based resizing +/// * Drag and drop to reorganize panes +/// * Hotkey support +/// * Configurable modifier keys +/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.) +/// +/// ## Example +/// +/// ``` +/// # use iced_pure::widget::{pane_grid, text}; +/// # +/// # type PaneGrid<'a, Message> = +/// # iced_pure::widget::PaneGrid<'a, Message, iced_native::renderer::Null>; +/// # +/// enum PaneState { +/// SomePane, +/// AnotherKindOfPane, +/// } +/// +/// enum Message { +/// PaneDragged(pane_grid::DragEvent), +/// PaneResized(pane_grid::ResizeEvent), +/// } +/// +/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); +/// +/// let pane_grid = +/// PaneGrid::new(&state, |pane, state| { +/// pane_grid::Content::new(match state { +/// PaneState::SomePane => text("This is some pane"), +/// PaneState::AnotherKindOfPane => text("This is another kind of pane"), +/// }) +/// }) +/// .on_drag(Message::PaneDragged) +/// .on_resize(10, Message::PaneResized); +/// ``` +#[allow(missing_debug_implementations)] +pub struct PaneGrid<'a, Message, Renderer> { + state: &'a state::Internal, + elements: Vec<(Pane, Content<'a, Message, Renderer>)>, + width: Length, + height: Length, + spacing: u16, + on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>, + on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, + on_resize: Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, + style_sheet: Box<dyn StyleSheet + 'a>, +} + +impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + /// Creates a [`PaneGrid`] with the given [`State`] and view function. + /// + /// The view function will be called to display each [`Pane`] present in the + /// [`State`]. + pub fn new<T>( + state: &'a State<T>, + view: impl Fn(Pane, &'a T) -> Content<'a, Message, Renderer>, + ) -> Self { + let elements = { + state + .panes + .iter() + .map(|(pane, pane_state)| (*pane, view(*pane, pane_state))) + .collect() + }; + + Self { + elements, + state: &state.internal, + width: Length::Fill, + height: Length::Fill, + spacing: 0, + on_click: None, + on_drag: None, + on_resize: None, + style_sheet: Default::default(), + } + } + + /// Sets the width of the [`PaneGrid`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`PaneGrid`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the spacing _between_ the panes of the [`PaneGrid`]. + pub fn spacing(mut self, units: u16) -> Self { + self.spacing = units; + self + } + + /// Sets the message that will be produced when a [`Pane`] of the + /// [`PaneGrid`] is clicked. + pub fn on_click<F>(mut self, f: F) -> Self + where + F: 'a + Fn(Pane) -> Message, + { + self.on_click = Some(Box::new(f)); + self + } + + /// Enables the drag and drop interactions of the [`PaneGrid`], which will + /// use the provided function to produce messages. + pub fn on_drag<F>(mut self, f: F) -> Self + where + F: 'a + Fn(DragEvent) -> Message, + { + self.on_drag = Some(Box::new(f)); + self + } + + /// Enables the resize interactions of the [`PaneGrid`], which will + /// use the provided function to produce messages. + /// + /// The `leeway` describes the amount of space around a split that can be + /// used to grab it. + /// + /// The grabbable area of a split will have a length of `spacing + leeway`, + /// properly centered. In other words, a length of + /// `(spacing + leeway) / 2.0` on either side of the split line. + pub fn on_resize<F>(mut self, leeway: u16, f: F) -> Self + where + F: 'a + Fn(ResizeEvent) -> Message, + { + self.on_resize = Some((leeway, Box::new(f))); + self + } + + /// Sets the style of the [`PaneGrid`]. + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet + 'a>>) -> Self { + self.style_sheet = style.into(); + self + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for PaneGrid<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<state::Action>() + } + + fn state(&self) -> tree::State { + tree::State::new(state::Action::Idle) + } + + fn children(&self) -> Vec<Tree> { + self.elements + .iter() + .map(|(_, content)| content.state()) + .collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children_custom( + &self.elements, + |(_, content), state| content.diff(state), + |(_, content)| content.state(), + ) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + pane_grid::layout( + renderer, + limits, + self.state, + self.width, + self.height, + self.spacing, + self.elements.iter().map(|(pane, content)| (*pane, content)), + |element, renderer, limits| element.layout(renderer, limits), + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let action = tree.state.downcast_mut::<state::Action>(); + + let event_status = pane_grid::update( + action, + self.state, + &event, + layout, + cursor_position, + shell, + self.spacing, + self.elements.iter().map(|(pane, content)| (*pane, content)), + &self.on_click, + &self.on_drag, + &self.on_resize, + ); + + let picked_pane = action.picked_pane().map(|(pane, _)| pane); + + self.elements + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|(((pane, content), tree), layout)| { + let is_picked = picked_pane == Some(*pane); + + content.on_event( + tree, + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + shell, + is_picked, + ) + }) + .fold(event_status, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + pane_grid::mouse_interaction( + tree.state.downcast_ref(), + self.state, + layout, + cursor_position, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + ) + .unwrap_or_else(|| { + self.elements + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|(((_pane, content), tree), layout)| { + content.mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + }) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + pane_grid::draw( + tree.state.downcast_ref(), + self.state, + layout, + cursor_position, + renderer, + style, + viewport, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + self.style_sheet.as_ref(), + self.elements + .iter() + .zip(&tree.children) + .map(|((pane, content), tree)| (*pane, (content, tree))), + |(content, tree), + renderer, + style, + layout, + cursor_position, + rectangle| { + content.draw( + tree, + renderer, + style, + layout, + cursor_position, + rectangle, + ); + }, + ) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.elements + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|(((_, pane), tree), layout)| { + pane.overlay(tree, layout, renderer) + }) + .next() + } +} + +impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: 'a + iced_native::Renderer, + Message: 'a, +{ + fn from( + pane_grid: PaneGrid<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(pane_grid) + } +} diff --git a/pure/src/widget/pane_grid/content.rs b/pure/src/widget/pane_grid/content.rs new file mode 100644 index 00000000..a928b28c --- /dev/null +++ b/pure/src/widget/pane_grid/content.rs @@ -0,0 +1,331 @@ +use crate::widget::pane_grid::TitleBar; +use crate::widget::tree::Tree; +use crate::Element; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::widget::container; +use iced_native::widget::pane_grid::Draggable; +use iced_native::{Clipboard, Layout, Point, Rectangle, Shell, Size}; + +/// The content of a [`Pane`]. +/// +/// [`Pane`]: crate::widget::pane_grid::Pane +#[allow(missing_debug_implementations)] +pub struct Content<'a, Message, Renderer> { + title_bar: Option<TitleBar<'a, Message, Renderer>>, + body: Element<'a, Message, Renderer>, + style_sheet: Box<dyn container::StyleSheet + 'a>, +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + /// Creates a new [`Content`] with the provided body. + pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self { + Self { + title_bar: None, + body: body.into(), + style_sheet: Default::default(), + } + } + + /// Sets the [`TitleBar`] of this [`Content`]. + pub fn title_bar( + mut self, + title_bar: TitleBar<'a, Message, Renderer>, + ) -> Self { + self.title_bar = Some(title_bar); + self + } + + /// Sets the style of the [`Content`]. + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn container::StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + pub fn state(&self) -> Tree { + let children = if let Some(title_bar) = self.title_bar.as_ref() { + vec![Tree::new(&self.body), title_bar.state()] + } else { + vec![Tree::new(&self.body), Tree::empty()] + }; + + Tree { + children, + ..Tree::empty() + } + } + + pub fn diff(&self, tree: &mut Tree) { + if tree.children.len() == 2 { + if let Some(title_bar) = self.title_bar.as_ref() { + title_bar.diff(&mut tree.children[1]); + } + + tree.children[0].diff(&self.body); + } else { + *tree = self.state(); + } + } + + /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. + /// + /// [`Renderer`]: crate::widget::pane_grid::Renderer + pub fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + { + let style = self.style_sheet.style(); + + container::draw_background(renderer, &style, bounds); + } + + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + let body_layout = children.next().unwrap(); + + let show_controls = bounds.contains(cursor_position); + + title_bar.draw( + &tree.children[1], + renderer, + style, + title_bar_layout, + cursor_position, + viewport, + show_controls, + ); + + self.body.as_widget().draw( + &tree.children[0], + renderer, + style, + body_layout, + cursor_position, + viewport, + ); + } else { + self.body.as_widget().draw( + &tree.children[0], + renderer, + style, + layout, + cursor_position, + viewport, + ); + } + } + + pub(crate) fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + if let Some(title_bar) = &self.title_bar { + let max_size = limits.max(); + + let title_bar_layout = title_bar + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let title_bar_size = title_bar_layout.size(); + + let mut body_layout = self.body.as_widget().layout( + renderer, + &layout::Limits::new( + Size::ZERO, + Size::new( + max_size.width, + max_size.height - title_bar_size.height, + ), + ), + ); + + body_layout.move_to(Point::new(0.0, title_bar_size.height)); + + layout::Node::with_children( + max_size, + vec![title_bar_layout, body_layout], + ) + } else { + self.body.as_widget().layout(renderer, limits) + } + } + + pub(crate) fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + is_picked: bool, + ) -> event::Status { + let mut event_status = event::Status::Ignored; + + let body_layout = if let Some(title_bar) = &mut self.title_bar { + let mut children = layout.children(); + + event_status = title_bar.on_event( + &mut tree.children[1], + event.clone(), + children.next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + ); + + children.next().unwrap() + } else { + layout + }; + + let body_status = if is_picked { + event::Status::Ignored + } else { + self.body.as_widget_mut().on_event( + &mut tree.children[0], + event, + body_layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }; + + event_status.merge(body_status) + } + + pub(crate) fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let (body_layout, title_bar_interaction) = + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + let is_over_pick_area = title_bar + .is_over_pick_area(title_bar_layout, cursor_position); + + if is_over_pick_area { + return mouse::Interaction::Grab; + } + + let mouse_interaction = title_bar.mouse_interaction( + &tree.children[1], + title_bar_layout, + cursor_position, + viewport, + renderer, + ); + + (children.next().unwrap(), mouse_interaction) + } else { + (layout, mouse::Interaction::default()) + }; + + self.body + .as_widget() + .mouse_interaction( + &tree.children[0], + body_layout, + cursor_position, + viewport, + renderer, + ) + .max(title_bar_interaction) + } + + pub(crate) fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + if let Some(title_bar) = self.title_bar.as_ref() { + let mut children = layout.children(); + let title_bar_layout = children.next()?; + + let mut states = tree.children.iter_mut(); + let body_state = states.next().unwrap(); + let title_bar_state = states.next().unwrap(); + + match title_bar.overlay(title_bar_state, title_bar_layout, renderer) + { + Some(overlay) => Some(overlay), + None => self.body.as_widget().overlay( + body_state, + children.next()?, + renderer, + ), + } + } else { + self.body.as_widget().overlay( + &mut tree.children[0], + layout, + renderer, + ) + } + } +} + +impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn can_be_dragged_at( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + title_bar.is_over_pick_area(title_bar_layout, cursor_position) + } else { + false + } + } +} + +impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer> +where + T: Into<Element<'a, Message, Renderer>>, + Renderer: iced_native::Renderer, +{ + fn from(element: T) -> Self { + Self::new(element) + } +} diff --git a/pure/src/widget/pane_grid/title_bar.rs b/pure/src/widget/pane_grid/title_bar.rs new file mode 100644 index 00000000..dd68b073 --- /dev/null +++ b/pure/src/widget/pane_grid/title_bar.rs @@ -0,0 +1,355 @@ +use crate::widget::Tree; +use crate::Element; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::widget::container; +use iced_native::{Clipboard, Layout, Padding, Point, Rectangle, Shell, Size}; + +/// The title bar of a [`Pane`]. +/// +/// [`Pane`]: crate::widget::pane_grid::Pane +#[allow(missing_debug_implementations)] +pub struct TitleBar<'a, Message, Renderer> { + content: Element<'a, Message, Renderer>, + controls: Option<Element<'a, Message, Renderer>>, + padding: Padding, + always_show_controls: bool, + style_sheet: Box<dyn container::StyleSheet + 'a>, +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + /// Creates a new [`TitleBar`] with the given content. + pub fn new<E>(content: E) -> Self + where + E: Into<Element<'a, Message, Renderer>>, + { + Self { + content: content.into(), + controls: None, + padding: Padding::ZERO, + always_show_controls: false, + style_sheet: Default::default(), + } + } + + /// Sets the controls of the [`TitleBar`]. + pub fn controls( + mut self, + controls: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { + self.controls = Some(controls.into()); + self + } + + /// Sets the [`Padding`] of the [`TitleBar`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the style of the [`TitleBar`]. + pub fn style( + mut self, + style: impl Into<Box<dyn container::StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style.into(); + self + } + + /// Sets whether or not the [`controls`] attached to this [`TitleBar`] are + /// always visible. + /// + /// By default, the controls are only visible when the [`Pane`] of this + /// [`TitleBar`] is hovered. + /// + /// [`controls`]: Self::controls + /// [`Pane`]: crate::widget::pane_grid::Pane + pub fn always_show_controls(mut self) -> Self { + self.always_show_controls = true; + self + } +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + pub fn state(&self) -> Tree { + let children = if let Some(controls) = self.controls.as_ref() { + vec![Tree::new(&self.content), Tree::new(controls)] + } else { + vec![Tree::new(&self.content), Tree::empty()] + }; + + Tree { + children, + ..Tree::empty() + } + } + + pub fn diff(&self, tree: &mut Tree) { + if tree.children.len() == 2 { + if let Some(controls) = self.controls.as_ref() { + tree.children[1].diff(controls); + } + + tree.children[0].diff(&self.content); + } else { + *tree = self.state(); + } + } + + /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. + /// + /// [`Renderer`]: crate::widget::pane_grid::Renderer + pub fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + show_controls: bool, + ) { + let bounds = layout.bounds(); + let style = self.style_sheet.style(); + let inherited_style = renderer::Style { + text_color: style.text_color.unwrap_or(inherited_style.text_color), + }; + + container::draw_background(renderer, &style, bounds); + + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + self.content.as_widget().draw( + &tree.children[0], + renderer, + &inherited_style, + title_layout, + cursor_position, + viewport, + ); + + if let Some(controls) = &self.controls { + let controls_layout = children.next().unwrap(); + + if show_controls || self.always_show_controls { + controls.as_widget().draw( + &tree.children[1], + renderer, + &inherited_style, + controls_layout, + cursor_position, + viewport, + ); + } + } + } + + /// Returns whether the mouse cursor is over the pick area of the + /// [`TitleBar`] or not. + /// + /// The whole [`TitleBar`] is a pick area, except its controls. + pub fn is_over_pick_area( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if layout.bounds().contains(cursor_position) { + let mut children = layout.children(); + let padded = children.next().unwrap(); + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + if self.controls.is_some() { + let controls_layout = children.next().unwrap(); + + !controls_layout.bounds().contains(cursor_position) + && !title_layout.bounds().contains(cursor_position) + } else { + !title_layout.bounds().contains(cursor_position) + } + } else { + false + } + } + + pub(crate) fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.pad(self.padding); + let max_size = limits.max(); + + let title_layout = self + .content + .as_widget() + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let title_size = title_layout.size(); + + let mut node = if let Some(controls) = &self.controls { + let mut controls_layout = controls + .as_widget() + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let controls_size = controls_layout.size(); + let space_before_controls = max_size.width - controls_size.width; + + let height = title_size.height.max(controls_size.height); + + controls_layout.move_to(Point::new(space_before_controls, 0.0)); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![title_layout, controls_layout], + ) + } else { + layout::Node::with_children( + Size::new(max_size.width, title_size.height), + vec![title_layout], + ) + }; + + node.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); + + layout::Node::with_children(node.size().pad(self.padding), vec![node]) + } + + pub(crate) fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + let control_status = if let Some(controls) = &mut self.controls { + let controls_layout = children.next().unwrap(); + + controls.as_widget_mut().on_event( + &mut tree.children[1], + event.clone(), + controls_layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } else { + event::Status::Ignored + }; + + let title_status = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + title_layout, + cursor_position, + renderer, + clipboard, + shell, + ); + + control_status.merge(title_status) + } + + pub(crate) fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + let title_interaction = self.content.as_widget().mouse_interaction( + &tree.children[0], + title_layout, + cursor_position, + viewport, + renderer, + ); + + if let Some(controls) = &self.controls { + let controls_layout = children.next().unwrap(); + + controls + .as_widget() + .mouse_interaction( + &tree.children[1], + controls_layout, + cursor_position, + viewport, + renderer, + ) + .max(title_interaction) + } else { + title_interaction + } + } + + pub(crate) fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let mut children = layout.children(); + let padded = children.next()?; + + let mut children = padded.children(); + let title_layout = children.next()?; + + let Self { + content, controls, .. + } = self; + + let mut states = tree.children.iter_mut(); + let title_state = states.next().unwrap(); + let controls_state = states.next().unwrap(); + + content + .as_widget() + .overlay(title_state, title_layout, renderer) + .or_else(move || { + controls.as_ref().and_then(|controls| { + let controls_layout = children.next()?; + + controls.as_widget().overlay( + controls_state, + controls_layout, + renderer, + ) + }) + }) + } +} diff --git a/pure/src/widget/tree.rs b/pure/src/widget/tree.rs index 33f5693a..3fcf0922 100644 --- a/pure/src/widget/tree.rs +++ b/pure/src/widget/tree.rs @@ -42,6 +42,19 @@ impl Tree { &mut self, new_children: &[Element<'_, Message, Renderer>], ) { + self.diff_children_custom( + new_children, + |new, child_state| child_state.diff(new), + Self::new, + ) + } + + pub fn diff_children_custom<T>( + &mut self, + new_children: &[T], + diff: impl Fn(&T, &mut Tree), + new_state: impl Fn(&T) -> Self, + ) { if self.children.len() > new_children.len() { self.children.truncate(new_children.len()); } @@ -49,12 +62,12 @@ impl Tree { for (child_state, new) in self.children.iter_mut().zip(new_children.iter()) { - child_state.diff(new); + diff(new, child_state); } if self.children.len() < new_children.len() { self.children.extend( - new_children[self.children.len()..].iter().map(Self::new), + new_children[self.children.len()..].iter().map(new_state), ); } } |