diff --git a/liana-gui/src/gui/mod.rs b/liana-gui/src/gui/mod.rs index 1dd51c0e..a9d9ae8c 100644 --- a/liana-gui/src/gui/mod.rs +++ b/liana-gui/src/gui/mod.rs @@ -1,8 +1,8 @@ use iced::{ event::{self, Event}, keyboard, - widget::{focus_next, focus_previous}, - Subscription, Task, + widget::{focus_next, focus_previous, pane_grid}, + Length, Subscription, Task, }; use tracing::{error, info}; use tracing_subscriber::filter::LevelFilter; @@ -10,7 +10,7 @@ extern crate serde; extern crate serde_json; use liana::miniscript::bitcoin; -use liana_ui::widget::{Column, Element}; +use liana_ui::widget::{Column, Container, Element}; pub mod pane; pub mod tab; @@ -18,7 +18,8 @@ pub mod tab; use crate::{dir::LianaDirectory, logger::Logger, VERSION}; pub struct GUI { - pane: pane::Pane, + panes: pane_grid::State, + focus: Option, config: Config, // We may change the directory of log outputs later _logger: Logger, @@ -33,9 +34,13 @@ pub enum Key { pub enum Message { CtrlC, FontLoaded(Result<(), iced::font::Error>), - Pane(pane::Message), + Pane(pane_grid::Pane, pane::Message), KeyPressed(Key), Event(iced::Event), + + Clicked(pane_grid::Pane), + Dragged(pane_grid::DragEvent), + Resized(pane_grid::ResizeEvent), } impl From> for Message { @@ -65,10 +70,12 @@ impl GUI { ); let mut cmds = vec![Task::perform(ctrl_c(), |_| Message::CtrlC)]; let (pane, cmd) = pane::Pane::new(&config); - cmds.push(cmd.map(Message::Pane)); + let (panes, focused_pane) = pane_grid::State::new(pane); + cmds.push(cmd.map(move |msg| Message::Pane(focused_pane, msg))); ( Self { - pane, + panes, + focus: Some(focused_pane), config, _logger: logger, }, @@ -80,7 +87,9 @@ impl GUI { match message { Message::CtrlC | Message::Event(iced::Event::Window(iced::window::Event::CloseRequested)) => { - self.pane.stop(); + for (_, pane) in self.panes.iter_mut() { + pane.stop(); + } iced::window::get_latest().and_then(iced::window::close) } Message::KeyPressed(Key::Tab(shift)) => { @@ -91,15 +100,84 @@ impl GUI { focus_next() } } - Message::Pane(msg) => self.pane.update(msg, &self.config).map(Message::Pane), + Message::Pane(pane_id, pane::Message::View(pane::ViewMessage::SplitTab(i))) => { + if let Some(p) = self.panes.get_mut(pane_id) { + let tab = p.remove_tab(i); + let result = self.panes.split( + pane_grid::Axis::Vertical, + pane_id, + pane::Pane::new_with_tab(tab.state), + ); + + if let Some((pane, _)) = result { + self.focus = Some(pane); + } + } + Task::none() + } + Message::Pane(pane_id, pane::Message::View(pane::ViewMessage::CloseTab(i))) => { + if let Some(pane) = self.panes.get_mut(pane_id) { + let _ = pane + .update( + pane::Message::View(pane::ViewMessage::CloseTab(i)), + &self.config, + ) + .map(move |msg| Message::Pane(pane_id, msg)); + if pane.tabs.is_empty() { + self.panes.close(pane_id); + if self.focus == Some(pane_id) { + self.focus = None; + } + } + } + if !self.panes.iter().any(|(_, p)| !p.tabs.is_empty()) { + return iced::window::get_latest().and_then(iced::window::close); + } + Task::none() + } + Message::Pane(i, msg) => { + if let Some(pane) = self.panes.get_mut(i) { + return pane + .update(msg, &self.config) + .map(move |msg| Message::Pane(i, msg)); + } + Task::none() + } + Message::Clicked(pane) => { + self.focus = Some(pane); + Task::none() + } + Message::Resized(pane_grid::ResizeEvent { split, ratio }) => { + self.panes.resize(split, ratio); + Task::none() + } + Message::Dragged(pane_grid::DragEvent::Dropped { pane, target }) => { + if let pane_grid::Target::Pane(p, pane_grid::Region::Center) = target { + let (tabs, focused_tab) = if let Some(origin) = self.panes.get_mut(pane) { + (std::mem::take(&mut origin.tabs), origin.focused_tab) + } else { + (Vec::new(), 0) + }; + + if let Some(dest) = self.panes.get_mut(p) { + if !tabs.is_empty() { + dest.add_tabs(tabs, focused_tab); + } + } + self.panes.close(pane); + self.focus = Some(p); + } else { + self.panes.drop(pane, target); + } + Task::none() + } _ => Task::none(), } } pub fn subscription(&self) -> Subscription { - Subscription::batch(vec![ - self.pane.subscription().map(Message::Pane), - iced::event::listen_with(|event, status, _| match (&event, status) { + let mut vec = vec![iced::event::listen_with(|event, status, _| { + match (&event, status) { ( Event::Keyboard(keyboard::Event::KeyPressed { key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Tab), @@ -113,14 +191,49 @@ impl GUI { event::Status::Ignored, ) => Some(Message::Event(event)), _ => None, - }), - ]) + } + })]; + for (id, pane) in self.panes.iter() { + vec.push( + pane.subscription() + .with(*id) + .map(|(id, msg)| Message::Pane(id, msg)), + ); + } + Subscription::batch(vec) } pub fn view(&self) -> Element { - Column::new() - .push(self.pane.tabs_menu_view().map(Message::Pane)) - .push(self.pane.view().map(Message::Pane)) + if self.panes.len() == 1 { + if let Some((&id, pane)) = self.panes.iter().nth(0) { + return Column::new() + .push(pane.tabs_menu_view().map(move |msg| Message::Pane(id, msg))) + .push(pane.view().map(move |msg| Message::Pane(id, msg))) + .into(); + } + } + + let focus = self.focus; + let pane_grid = pane_grid::PaneGrid::new(&self.panes, |id, pane, _| { + let _is_focused = focus == Some(id); + + pane_grid::Content::new(pane.view().map(move |msg| Message::Pane(id, msg))).title_bar( + pane_grid::TitleBar::new( + pane.tabs_menu_view().map(move |msg| Message::Pane(id, msg)), + ), + ) + }) + .spacing(10) + .width(Length::Fill) + .height(Length::Fill) + .on_click(Message::Clicked) + .on_drag(Message::Dragged) + .on_resize(10, Message::Resized); + + Container::new(pane_grid) + .style(liana_ui::theme::pane_grid::pane_grid_background) + .width(Length::Fill) + .height(Length::Fill) .into() } diff --git a/liana-gui/src/gui/pane.rs b/liana-gui/src/gui/pane.rs index edd57f31..d19e2d69 100644 --- a/liana-gui/src/gui/pane.rs +++ b/liana-gui/src/gui/pane.rs @@ -1,4 +1,4 @@ -use iced::{Subscription, Task}; +use iced::{Length, Subscription, Task}; use iced_aw::ContextMenu; use liana_ui::{component::text::*, icon::plus_icon, theme, widget::*}; @@ -16,14 +16,15 @@ pub enum Message { pub enum ViewMessage { FocusTab(usize), CloseTab(usize), + SplitTab(usize), AddTab, } pub struct Pane { - tabs: Vec, + pub tabs: Vec, // this is an index in the tabs array - focused_tab: usize, + pub focused_tab: usize, // used to generate tabs ids. tabs_created: usize, @@ -42,6 +43,14 @@ impl Pane { ) } + pub fn new_with_tab(s: tab::State) -> Self { + Self { + tabs: vec![tab::Tab::new(1, s)], + focused_tab: 0, + tabs_created: 1, + } + } + fn add_tab(&mut self, cfg: &Config) -> Task { let (state, task) = tab::State::new(cfg.liana_directory.clone(), cfg.network); self.tabs_created += 1; @@ -51,9 +60,13 @@ impl Pane { task.map(move |msg| Message::Tab(id, msg)) } - fn remove_tab(&mut self, i: usize) { - let mut tab = self.tabs.remove(i); + fn close_tab(&mut self, i: usize) { + let mut tab = self.remove_tab(i); tab.stop(); + } + + pub fn remove_tab(&mut self, i: usize) -> tab::Tab { + let tab = self.tabs.remove(i); self.focused_tab = if self.tabs.is_empty() { 0 } else if i < self.tabs.len() - 1 { @@ -61,6 +74,18 @@ impl Pane { } else { self.tabs.len() - 1 }; + tab + } + + pub fn add_tabs(&mut self, tabs: Vec, focused_tab: usize) { + for tab in tabs { + self.tabs_created += 1; + let id = self.tabs_created; + self.tabs.push(tab::Tab::new(id, tab.state)); + } + if self.focused_tab + focused_tab + 1 < self.tabs.len() { + self.focused_tab += focused_tab + 1; + } } pub fn update(&mut self, message: Message, cfg: &Config) -> Task { @@ -79,9 +104,11 @@ impl Pane { } Message::View(ViewMessage::AddTab) => self.add_tab(cfg), Message::View(ViewMessage::CloseTab(i)) => { - self.remove_tab(i); + self.close_tab(i); Task::none() } + // handle by the pane grid update. + Message::View(ViewMessage::SplitTab(_)) => Task::none(), } } @@ -104,6 +131,7 @@ impl Pane { pub fn tabs_menu_view(&self) -> Element { let mut menu = Row::new().spacing(3); + let tabs_len = self.tabs.len(); for (i, tab) in self.tabs.iter().enumerate() { let title = tab.title(); menu = menu.push(ContextMenu::new( @@ -125,10 +153,23 @@ impl Pane { .on_press(ViewMessage::FocusTab(i)), ), move || { - Button::new(p1_regular("Close")) - .style(theme::button::secondary) - .on_press(ViewMessage::CloseTab(i)) - .width(100) + Column::new() + .push( + Button::new(p1_regular("Close")) + .style(theme::button::secondary) + .on_press(ViewMessage::CloseTab(i)) + .width(100), + ) + .push_maybe(if tabs_len > 1 { + Some( + Button::new(p1_regular("Split")) + .style(theme::button::secondary) + .on_press(ViewMessage::SplitTab(i)) + .width(100), + ) + } else { + None + }) .into() }, )); @@ -142,11 +183,15 @@ impl Pane { } pub fn view(&self) -> Element { - if let Some(t) = self.tabs.get(self.focused_tab) { + Container::new(if let Some(t) = self.tabs.get(self.focused_tab) { let id = t.id; t.view().map(move |msg| Message::Tab(id, msg)) } else { Row::new().into() - } + }) + .style(theme::container::background) + .width(Length::Fill) + .height(Length::Fill) + .into() } } diff --git a/liana-ui/src/color.rs b/liana-ui/src/color.rs index 54b9166e..31af9f64 100644 --- a/liana-ui/src/color.rs +++ b/liana-ui/src/color.rs @@ -47,6 +47,12 @@ pub const GREEN: Color = Color::from_rgb( 0xFF as f32 / 255.0, 0x66 as f32 / 255.0, ); +pub const TRANSPARENT_GREEN: Color = Color::from_rgba( + 0x00 as f32 / 255.0, + 0xFF as f32 / 255.0, + 0x66 as f32 / 255.0, + 0.3, +); pub const RED: Color = Color::from_rgb( 0xE2 as f32 / 255.0, 0x4E as f32 / 255.0, diff --git a/liana-ui/src/theme/button.rs b/liana-ui/src/theme/button.rs index 12751956..9e7951a2 100644 --- a/liana-ui/src/theme/button.rs +++ b/liana-ui/src/theme/button.rs @@ -147,7 +147,7 @@ fn button(p: &Button, status: Status) -> Style { } pub fn tab(theme: &Theme, status: Status) -> Style { - let mut style = button(&theme.colors.buttons.secondary, status); + let mut style = button(&theme.colors.buttons.tab, status); style.border.radius = 0.0.into(); style.border.width = 0.0; style diff --git a/liana-ui/src/theme/mod.rs b/liana-ui/src/theme/mod.rs index c1d5e26d..8184191d 100644 --- a/liana-ui/src/theme/mod.rs +++ b/liana-ui/src/theme/mod.rs @@ -8,6 +8,7 @@ pub mod context_menu; pub mod notification; pub mod overlay; pub mod palette; +pub mod pane_grid; pub mod pick_list; pub mod pill; pub mod progress_bar; diff --git a/liana-ui/src/theme/palette.rs b/liana-ui/src/theme/palette.rs index 1833c36b..6387705a 100644 --- a/liana-ui/src/theme/palette.rs +++ b/liana-ui/src/theme/palette.rs @@ -16,6 +16,7 @@ pub struct Palette { pub sliders: Sliders, pub progress_bars: ProgressBars, pub rule: iced::Color, + pub pane_grid: PaneGrid, } #[derive(Debug, Copy, Clone, PartialEq)] @@ -44,6 +45,7 @@ pub struct Buttons { pub container: Button, pub container_border: Button, pub menu: Button, + pub tab: Button, } #[derive(Debug, Copy, Clone, PartialEq)] @@ -161,6 +163,15 @@ pub struct ProgressBars { pub border: Option, } +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct PaneGrid { + pub background: iced::Color, + pub highlight_border: iced::Color, + pub highlight_background: iced::Color, + pub picked_split: iced::Color, + pub hovered_split: iced::Color, +} + impl std::default::Default for Palette { fn default() -> Self { Self { @@ -353,6 +364,28 @@ impl std::default::Default for Palette { border: color::TRANSPARENT.into(), }), }, + tab: Button { + active: ButtonPalette { + background: color::GREY_6, + text: color::GREY_2, + border: color::GREY_7.into(), + }, + hovered: ButtonPalette { + background: color::GREY_6, + text: color::GREEN, + border: color::GREEN.into(), + }, + pressed: Some(ButtonPalette { + background: color::LIGHT_BLACK, + text: color::GREEN, + border: color::GREEN.into(), + }), + disabled: Some(ButtonPalette { + background: color::GREY_6, + text: color::GREY_2, + border: color::GREY_7.into(), + }), + }, }, cards: Cards { simple: ContainerPalette { @@ -505,6 +538,13 @@ impl std::default::Default for Palette { background: color::GREY_6, }, rule: color::GREY_1, + pane_grid: PaneGrid { + background: color::BLACK, + highlight_border: color::GREEN, + highlight_background: color::TRANSPARENT_GREEN, + picked_split: color::GREEN, + hovered_split: color::GREEN, + }, } } } diff --git a/liana-ui/src/theme/pane_grid.rs b/liana-ui/src/theme/pane_grid.rs new file mode 100644 index 00000000..bbe3bf0d --- /dev/null +++ b/liana-ui/src/theme/pane_grid.rs @@ -0,0 +1,45 @@ +use iced::widget::container; +use iced::widget::pane_grid::{Catalog, Highlight, Line, Style, StyleFn}; +use iced::Border; + +use super::Theme; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> ::Class<'a> { + Box::new(primary) + } + + fn style(&self, class: &::Class<'_>) -> Style { + class(self) + } +} + +pub fn primary(theme: &Theme) -> Style { + Style { + hovered_region: Highlight { + background: theme.colors.pane_grid.highlight_background.into(), + border: Border { + color: theme.colors.pane_grid.highlight_border, + width: 1.0, + radius: 0.0.into(), + }, + }, + picked_split: Line { + color: theme.colors.pane_grid.picked_split, + width: 2.0, + }, + hovered_split: Line { + color: theme.colors.pane_grid.hovered_split, + width: 2.0, + }, + } +} + +pub fn pane_grid_background(theme: &Theme) -> container::Style { + container::Style { + background: Some(theme.colors.pane_grid.background.into()), + ..Default::default() + } +}