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, panes_created: usize, count: usize, _focused: window::Id, } #[derive(Debug)] struct Window { title: String, scale: f64, theme: Theme, panes: pane_grid::State, focus: Option, } #[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) { 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 { 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 { 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 { 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 { 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, ) -> 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() } } }