rae 0.1.13

Renderer-neutral Rust design catalog for desktop widgets, layouts, sanitized output, and GLSL shader primitives.
Documentation
use iced::keyboard;
use iced::widget::pane_grid::{self, PaneGrid};
use iced::widget::{button, column, container, responsive, row, scrollable, text};
use iced::{Center, Element, Fill, Length, Size, Subscription, Task};

pub fn main() -> iced::Result {
    iced::application("rae pane grid", Example::update, Example::view)
        .subscription(Example::subscription)
        .window_size(Size::new(960.0, 640.0))
        .run_with(|| (Example::new(), Task::none()))
}

struct Example {
    panes: pane_grid::State<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),
    Maximize(pane_grid::Pane),
    Restore,
    Close(pane_grid::Pane),
    CloseFocused,
}

impl Example {
    fn new() -> Self {
        let (panes, _) = pane_grid::State::new(Pane::new(0));

        Self {
            panes,
            panes_created: 1,
            focus: None,
        }
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::Split(axis, pane) => {
                if let Some((pane, _)) = self.panes.split(axis, pane, Pane::new(self.panes_created))
                {
                    self.focus = Some(pane);
                }

                self.panes_created += 1;
            }
            Message::SplitFocused(axis) => {
                if let Some(pane) = self.focus {
                    if let Some((pane, _)) =
                        self.panes.split(axis, pane, Pane::new(self.panes_created))
                    {
                        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.drop(pane, target);
            }
            Message::Dragged(_) => {}
            Message::TogglePin(pane) => {
                if let Some(Pane { is_pinned, .. }) = self.panes.get_mut(pane) {
                    *is_pinned = !*is_pinned;
                }
            }
            Message::Maximize(pane) => {
                self.panes.maximize(pane);
            }
            Message::Restore => {
                self.panes.restore();
            }
            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);
                            }
                        }
                    }
                }
            }
        }
    }

    fn subscription(&self) -> Subscription<Message> {
        keyboard::on_key_press(|key, modifiers| {
            if modifiers.command() {
                handle_hotkey(key)
            } else {
                None
            }
        })
    }

    fn view(&self) -> Element<'_, Message> {
        let focus = self.focus;
        let total_panes = self.panes.len();

        let pane_grid = PaneGrid::new(&self.panes, |id, pane, is_maximized| {
            let title = row![
                button(if pane.is_pinned { "Unpin" } else { "Pin" })
                    .on_press(Message::TogglePin(id))
                    .padding(3),
                text(format!("Pane {}", pane.id)).size(16),
                if focus == Some(id) {
                    text("focused").size(14)
                } else {
                    text("").size(14)
                },
            ]
            .spacing(8)
            .align_y(Center);

            let title_bar = pane_grid::TitleBar::new(title)
                .controls(pane_grid::Controls::dynamic(
                    view_controls(id, total_panes, pane.is_pinned, is_maximized),
                    button("X").padding(3).on_press_maybe(
                        (total_panes > 1 && !pane.is_pinned).then_some(Message::Close(id)),
                    ),
                ))
                .padding(8);

            pane_grid::Content::new(responsive(move |size| {
                view_content(id, total_panes, pane.is_pinned, size)
            }))
            .title_bar(title_bar)
        })
        .width(Fill)
        .height(Fill)
        .spacing(10)
        .on_click(Message::Clicked)
        .on_drag(Message::Dragged)
        .on_resize(10, Message::Resized);

        container(pane_grid).padding(10).into()
    }
}

#[derive(Clone, Copy)]
struct Pane {
    id: usize,
    is_pinned: bool,
}

impl Pane {
    fn new(id: usize) -> Self {
        Self {
            id,
            is_pinned: false,
        }
    }
}

fn handle_hotkey(key: keyboard::Key) -> Option<Message> {
    use iced::keyboard::key::{self, Key};
    use pane_grid::{Axis, Direction};

    match key.as_ref() {
        Key::Character("v") => Some(Message::SplitFocused(Axis::Vertical)),
        Key::Character("h") => Some(Message::SplitFocused(Axis::Horizontal)),
        Key::Character("w") => Some(Message::CloseFocused),
        Key::Named(key) => match key {
            key::Named::ArrowUp => Some(Message::FocusAdjacent(Direction::Up)),
            key::Named::ArrowDown => Some(Message::FocusAdjacent(Direction::Down)),
            key::Named::ArrowLeft => Some(Message::FocusAdjacent(Direction::Left)),
            key::Named::ArrowRight => Some(Message::FocusAdjacent(Direction::Right)),
            _ => None,
        },
        _ => None,
    }
}

fn view_content<'a>(
    pane: pane_grid::Pane,
    total_panes: usize,
    is_pinned: bool,
    size: Size,
) -> Element<'a, Message> {
    let action = |label, message| {
        button(text(label).width(Fill).align_x(Center).size(16))
            .padding(8)
            .on_press(message)
    };

    let mut controls = column![
        action(
            "Split horizontally",
            Message::Split(pane_grid::Axis::Horizontal, pane),
        ),
        action(
            "Split vertically",
            Message::Split(pane_grid::Axis::Vertical, pane),
        ),
    ];

    if total_panes > 1 && !is_pinned {
        controls = controls.push(action("Close", Message::Close(pane)));
    }

    let controls = controls.spacing(6).width(Length::Fixed(190.0));

    let content = column![
        text(format!("{:.0} x {:.0}", size.width, size.height)).size(24),
        controls,
    ]
    .spacing(12)
    .width(Fill)
    .align_x(Center);

    container(scrollable(content)).padding(8).into()
}

fn view_controls<'a>(
    pane: pane_grid::Pane,
    total_panes: usize,
    is_pinned: bool,
    is_maximized: bool,
) -> Element<'a, Message> {
    let mut controls = row![].spacing(6);

    if total_panes > 1 {
        let (label, message) = if is_maximized {
            ("Restore", Message::Restore)
        } else {
            ("Maximize", Message::Maximize(pane))
        };

        controls = controls.push(button(label).padding(3).on_press(message));
    }

    let close = button("Close")
        .padding(3)
        .on_press_maybe((total_panes > 1 && !is_pinned).then_some(Message::Close(pane)));

    controls.push(close).into()
}