summaryrefslogblamecommitdiffstats
path: root/examples/multi_window_panes/src/main.rs
blob: b1d0a3bcb1fc6791ee48e161e6cf8f28e2d96b30 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                       







                                                                            
                           

























                                                                          
                 










































                                                                             
                                    

























































































                                                                          



                                                               








                                                                             

                                                                


























                                                                                



                                                  
                                                                            




                                                                   







                                                                            


                           


                                                                    
















































                                                                           


                                                  


















































































































                                                                                



                                                                           






































































































































































































































                                                                            
use iced::alignment::{self, Alignment};
use iced::keyboard;
use iced::multi_window::Application;
use iced::theme::{self, Theme};
use iced::widget::pane_grid::{self, PaneGrid};
use iced::widget::{
    button, column, container, pick_list, row, scrollable, text, text_input,
};
use iced::window;
use iced::{executor, time};
use iced::{Color, Command, Element, Length, Settings, Size, Subscription};
use iced_lazy::responsive;
use iced_native::{event, subscription, Event};

use iced_native::widget::scrollable::{Properties, RelativeOffset};
use iced_native::window::Id;
use std::collections::HashMap;
use std::time::{Duration, Instant};

pub fn main() -> iced::Result {
    env_logger::init();

    Example::run(Settings::default())
}

struct Example {
    windows: HashMap<window::Id, Window>,
    panes_created: usize,
    count: usize,
    _focused: window::Id,
}

#[derive(Debug)]
struct Window {
    title: String,
    scale: f64,
    theme: Theme,
    panes: pane_grid::State<Pane>,
    focus: Option<pane_grid::Pane>,
}

#[derive(Debug, Clone)]
enum Message {
    Window(window::Id, WindowMessage),
    CountIncremented(Instant),
}

#[derive(Debug, Clone)]
enum WindowMessage {
    Split(pane_grid::Axis, pane_grid::Pane),
    SplitFocused(pane_grid::Axis),
    FocusAdjacent(pane_grid::Direction),
    Clicked(pane_grid::Pane),
    Dragged(pane_grid::DragEvent),
    PopOut(pane_grid::Pane),
    Resized(pane_grid::ResizeEvent),
    TitleChanged(String),
    ToggleMoving(pane_grid::Pane),
    TogglePin(pane_grid::Pane),
    Close(pane_grid::Pane),
    CloseFocused,
    SelectedWindow(pane_grid::Pane, SelectableWindow),
    CloseWindow,
    SnapToggle,
}

impl Application for Example {
    type Executor = executor::Default;
    type Message = Message;
    type Theme = Theme;
    type Flags = ();

    fn new(_flags: ()) -> (Self, Command<Message>) {
        let (panes, _) =
            pane_grid::State::new(Pane::new(0, pane_grid::Axis::Horizontal));
        let window = Window {
            panes,
            focus: None,
            title: String::from("Default window"),
            scale: 1.0,
            theme: Theme::default(),
        };

        (
            Example {
                windows: HashMap::from([(window::Id::MAIN, window)]),
                panes_created: 1,
                count: 0,
                _focused: window::Id::MAIN,
            },
            Command::none(),
        )
    }

    fn title(&self, window: window::Id) -> String {
        self.windows
            .get(&window)
            .map(|w| w.title.clone())
            .unwrap_or(String::from("New Window"))
    }

    fn update(&mut self, message: Message) -> Command<Message> {
        match message {
            Message::Window(id, message) => match message {
                WindowMessage::SnapToggle => {
                    let window = self.windows.get_mut(&id).unwrap();

                    if let Some(focused) = &window.focus {
                        let pane = window.panes.get_mut(focused).unwrap();

                        let cmd = scrollable::snap_to(
                            pane.scrollable_id.clone(),
                            if pane.snapped {
                                RelativeOffset::START
                            } else {
                                RelativeOffset::END
                            },
                        );

                        pane.snapped = !pane.snapped;
                        return cmd;
                    }
                }
                WindowMessage::Split(axis, pane) => {
                    let window = self.windows.get_mut(&id).unwrap();
                    let result = window.panes.split(
                        axis,
                        &pane,
                        Pane::new(self.panes_created, axis),
                    );

                    if let Some((pane, _)) = result {
                        window.focus = Some(pane);
                    }

                    self.panes_created += 1;
                }
                WindowMessage::SplitFocused(axis) => {
                    let window = self.windows.get_mut(&id).unwrap();
                    if let Some(pane) = window.focus {
                        let result = window.panes.split(
                            axis,
                            &pane,
                            Pane::new(self.panes_created, axis),
                        );

                        if let Some((pane, _)) = result {
                            window.focus = Some(pane);
                        }

                        self.panes_created += 1;
                    }
                }
                WindowMessage::FocusAdjacent(direction) => {
                    let window = self.windows.get_mut(&id).unwrap();
                    if let Some(pane) = window.focus {
                        if let Some(adjacent) =
                            window.panes.adjacent(&pane, direction)
                        {
                            window.focus = Some(adjacent);
                        }
                    }
                }
                WindowMessage::Clicked(pane) => {
                    let window = self.windows.get_mut(&id).unwrap();
                    window.focus = Some(pane);
                }
                WindowMessage::CloseWindow => {
                    let _ = self.windows.remove(&id);
                    return window::close(id);
                }
                WindowMessage::Resized(pane_grid::ResizeEvent {
                    split,
                    ratio,
                }) => {
                    let window = self.windows.get_mut(&id).unwrap();
                    window.panes.resize(&split, ratio);
                }
                WindowMessage::SelectedWindow(pane, selected) => {
                    let window = self.windows.get_mut(&id).unwrap();
                    let (mut pane, _) = window.panes.close(&pane).unwrap();
                    pane.is_moving = false;

                    if let Some(window) = self.windows.get_mut(&selected.0) {
                        let (&first_pane, _) =
                            window.panes.iter().next().unwrap();
                        let result =
                            window.panes.split(pane.axis, &first_pane, pane);

                        if let Some((pane, _)) = result {
                            window.focus = Some(pane);
                        }
                    }
                }
                WindowMessage::ToggleMoving(pane) => {
                    let window = self.windows.get_mut(&id).unwrap();
                    if let Some(pane) = window.panes.get_mut(&pane) {
                        pane.is_moving = !pane.is_moving;
                    }
                }
                WindowMessage::TitleChanged(title) => {
                    let window = self.windows.get_mut(&id).unwrap();
                    window.title = title;
                }
                WindowMessage::PopOut(pane) => {
                    let window = self.windows.get_mut(&id).unwrap();
                    if let Some((popped, sibling)) = window.panes.close(&pane) {
                        window.focus = Some(sibling);

                        let (panes, _) = pane_grid::State::new(popped);
                        let window = Window {
                            panes,
                            focus: None,
                            title: format!(
                                "New window ({})",
                                self.windows.len()
                            ),
                            scale: 1.0 + (self.windows.len() as f64 / 10.0),
                            theme: if self.windows.len() % 2 == 0 {
                                Theme::Light
                            } else {
                                Theme::Dark
                            },
                        };

                        let window_id = window::Id::new(self.windows.len());
                        self.windows.insert(window_id, window);
                        return window::spawn(window_id, Default::default());
                    }
                }
                WindowMessage::Dragged(pane_grid::DragEvent::Dropped {
                    pane,
                    target,
                }) => {
                    let window = self.windows.get_mut(&id).unwrap();
                    window.panes.swap(&pane, &target);
                }
                WindowMessage::Dragged(_) => {}
                WindowMessage::TogglePin(pane) => {
                    let window = self.windows.get_mut(&id).unwrap();
                    if let Some(Pane { is_pinned, .. }) =
                        window.panes.get_mut(&pane)
                    {
                        *is_pinned = !*is_pinned;
                    }
                }
                WindowMessage::Close(pane) => {
                    let window = self.windows.get_mut(&id).unwrap();
                    if let Some((_, sibling)) = window.panes.close(&pane) {
                        window.focus = Some(sibling);
                    }
                }
                WindowMessage::CloseFocused => {
                    let window = self.windows.get_mut(&id).unwrap();
                    if let Some(pane) = window.focus {
                        if let Some(Pane { is_pinned, .. }) =
                            window.panes.get(&pane)
                        {
                            if !is_pinned {
                                if let Some((_, sibling)) =
                                    window.panes.close(&pane)
                                {
                                    window.focus = Some(sibling);
                                }
                            }
                        }
                    }
                }
            },
            Message::CountIncremented(_) => {
                self.count += 1;
            }
        }

        Command::none()
    }

    fn subscription(&self) -> Subscription<Message> {
        Subscription::batch(vec![
            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).map(|message| {
                            Message::Window(window::Id::new(0usize), message)
                        })
                    } // TODO(derezzedex)
                    _ => None,
                }
            }),
            time::every(Duration::from_secs(1)).map(Message::CountIncremented),
        ])
    }

    fn view(&self, window: window::Id) -> Element<Message> {
        let window_id = window;

        if let Some(window) = self.windows.get(&window) {
            let focus = window.focus;
            let total_panes = window.panes.len();

            let window_controls = row![
                text_input(
                    "Window title",
                    &window.title,
                    WindowMessage::TitleChanged,
                ),
                button(text("Close"))
                    .on_press(WindowMessage::CloseWindow)
                    .style(theme::Button::Destructive),
            ]
            .spacing(5)
            .align_items(Alignment::Center);

            let pane_grid = PaneGrid::new(&window.panes, |id, pane, _| {
                let is_focused = focus == Some(id);

                let pin_button = button(
                    text(if pane.is_pinned { "Unpin" } else { "Pin" }).size(14),
                )
                .on_press(WindowMessage::TogglePin(id))
                .padding(3);

                let title = row![
                    pin_button,
                    "Pane",
                    text(pane.id.to_string()).style(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,
                        pane.is_pinned,
                        pane.is_moving,
                        &window.title,
                        window_id,
                        &self.windows,
                    ))
                    .padding(10)
                    .style(if is_focused {
                        style::title_bar_focused
                    } else {
                        style::title_bar_active
                    });

                pane_grid::Content::new(responsive(move |size| {
                    view_content(
                        id,
                        pane.scrollable_id.clone(),
                        self.count,
                        total_panes,
                        pane.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(WindowMessage::Clicked)
            .on_drag(WindowMessage::Dragged)
            .on_resize(10, WindowMessage::Resized);

            let content: Element<_> = column![window_controls, pane_grid]
                .width(Length::Fill)
                .height(Length::Fill)
                .padding(10)
                .into();

            return content
                .map(move |message| Message::Window(window_id, message));
        }

        container(text("This shouldn't be possible!").size(20))
            .center_x()
            .center_y()
            .into()
    }

    fn close_requested(&self, window: window::Id) -> Self::Message {
        Message::Window(window, WindowMessage::CloseWindow)
    }

    fn scale_factor(&self, window: Id) -> f64 {
        self.windows.get(&window).map(|w| w.scale).unwrap_or(1.0)
    }

    fn theme(&self, window: Id) -> Theme {
        self.windows.get(&window).expect("Window not found!").theme.clone()
    }
}

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<WindowMessage> {
    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(WindowMessage::SplitFocused(Axis::Vertical)),
        KeyCode::H => Some(WindowMessage::SplitFocused(Axis::Horizontal)),
        KeyCode::W => Some(WindowMessage::CloseFocused),
        _ => direction.map(WindowMessage::FocusAdjacent),
    }
}

#[derive(Debug, Clone)]
struct SelectableWindow(window::Id, String);

impl PartialEq for SelectableWindow {
    fn eq(&self, other: &Self) -> bool {
        self.0 == other.0
    }
}

impl Eq for SelectableWindow {}

impl std::fmt::Display for SelectableWindow {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.1.fmt(f)
    }
}

#[derive(Debug)]
struct Pane {
    id: usize,
    pub scrollable_id: scrollable::Id,
    pub axis: pane_grid::Axis,
    pub is_pinned: bool,
    pub is_moving: bool,
    pub snapped: bool,
}

impl Pane {
    fn new(id: usize, axis: pane_grid::Axis) -> Self {
        Self {
            id,
            scrollable_id: scrollable::Id::unique(),
            axis,
            is_pinned: false,
            is_moving: false,
            snapped: false,
        }
    }
}

fn view_content<'a>(
    pane: pane_grid::Pane,
    scrollable_id: scrollable::Id,
    count: usize,
    total_panes: usize,
    is_pinned: bool,
    size: Size,
) -> Element<'a, WindowMessage> {
    let button = |label, message| {
        button(
            text(label)
                .width(Length::Fill)
                .horizontal_alignment(alignment::Horizontal::Center)
                .size(16),
        )
        .width(Length::Fill)
        .padding(8)
        .on_press(message)
    };

    let mut controls = column![
        button(
            "Split horizontally",
            WindowMessage::Split(pane_grid::Axis::Horizontal, pane),
        ),
        button(
            "Split vertically",
            WindowMessage::Split(pane_grid::Axis::Vertical, pane),
        ),
        button("Snap", WindowMessage::SnapToggle,)
    ]
    .spacing(5)
    .max_width(150);

    if total_panes > 1 && !is_pinned {
        controls = controls.push(
            button("Close", WindowMessage::Close(pane))
                .style(theme::Button::Destructive),
        );
    }

    let content = column![
        text(format!("{}x{}", size.width, size.height)).size(24),
        controls,
        text(format!("{count}")).size(48),
    ]
    .width(Length::Fill)
    .height(800)
    .spacing(10)
    .align_items(Alignment::Center);

    container(
        scrollable(content)
            .height(Length::Fill)
            .vertical_scroll(Properties::new())
            .id(scrollable_id),
    )
    .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,
    is_moving: bool,
    window_title: &'a str,
    window_id: window::Id,
    windows: &HashMap<window::Id, Window>,
) -> Element<'a, WindowMessage> {
    let window_selector = {
        let options: Vec<_> = windows
            .iter()
            .map(|(id, window)| SelectableWindow(*id, window.title.clone()))
            .collect();
        pick_list(
            options,
            Some(SelectableWindow(window_id, window_title.to_string())),
            move |window| WindowMessage::SelectedWindow(pane, window),
        )
    };

    let mut move_to = button(text("Move to").size(14)).padding(3);

    let mut pop_out = button(text("Pop Out").size(14)).padding(3);

    let mut close = button(text("Close").size(14))
        .style(theme::Button::Destructive)
        .padding(3);

    if total_panes > 1 && !is_pinned {
        close = close.on_press(WindowMessage::Close(pane));
        pop_out = pop_out.on_press(WindowMessage::PopOut(pane));
    }

    if windows.len() > 1 && total_panes > 1 && !is_pinned {
        move_to = move_to.on_press(WindowMessage::ToggleMoving(pane));
    }

    let mut content = row![].spacing(10);
    if is_moving {
        content = content.push(pop_out).push(window_selector).push(close);
    } else {
        content = content.push(pop_out).push(move_to).push(close);
    }

    content.into()
}

mod style {
    use iced::widget::container;
    use iced::Theme;

    pub fn title_bar_active(theme: &Theme) -> container::Appearance {
        let palette = theme.extended_palette();

        container::Appearance {
            text_color: Some(palette.background.strong.text),
            background: Some(palette.background.strong.color.into()),
            ..Default::default()
        }
    }

    pub fn title_bar_focused(theme: &Theme) -> container::Appearance {
        let palette = theme.extended_palette();

        container::Appearance {
            text_color: Some(palette.primary.strong.text),
            background: Some(palette.primary.strong.color.into()),
            ..Default::default()
        }
    }

    pub fn pane_active(theme: &Theme) -> container::Appearance {
        let palette = theme.extended_palette();

        container::Appearance {
            background: Some(palette.background.weak.color.into()),
            border_width: 2.0,
            border_color: palette.background.strong.color,
            ..Default::default()
        }
    }

    pub fn pane_focused(theme: &Theme) -> container::Appearance {
        let palette = theme.extended_palette();

        container::Appearance {
            background: Some(palette.background.weak.color.into()),
            border_width: 2.0,
            border_color: palette.primary.strong.color,
            ..Default::default()
        }
    }
}