Add grid with split button

This commit is contained in:
edouardparis 2025-06-23 15:01:55 +02:00
parent 9f1de060eb
commit 2866b8ea18
7 changed files with 280 additions and 30 deletions

View File

@ -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<pane::Pane>,
focus: Option<pane_grid::Pane>,
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<Result<(), iced::font::Error>> 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<Message> {
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<Message> {
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()
}

View File

@ -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<tab::Tab>,
pub tabs: Vec<tab::Tab>,
// 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<Message> {
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<tab::Tab>, 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<Message> {
@ -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<Message> {
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<Message> {
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()
}
}

View File

@ -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,

View File

@ -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

View File

@ -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;

View File

@ -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<iced::Color>,
}
#[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,
},
}
}
}

View File

@ -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>() -> <Self as Catalog>::Class<'a> {
Box::new(primary)
}
fn style(&self, class: &<Self as Catalog>::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()
}
}