From 7743d0d084666f3ec32076bef34a4e014ab15e59 Mon Sep 17 00:00:00 2001 From: Eduardo Flores Date: Sun, 9 Oct 2022 11:25:46 -0700 Subject: [PATCH] Implemented Expander - Updated example to show behavior - Created styles for Expander and ExpanderRow - Simpler implementation of `ExpanderRow` - Deleted `ExpanderData` and replaced it with `ExpanderRow` - Every row can now have child rows. - Ran cargo fmt. - Deleted settings example - Added expander to cosmic example - Expander icons now render ListBox partially implemented --- .gitignore | 1 + examples/cosmic/src/main.rs | 298 +----------------- examples/cosmic/src/window.rs | 307 +++++++++++++++++++ examples/settings/Cargo.toml | 9 - examples/settings/src/main.rs | 10 - examples/settings/src/window.rs | 150 --------- examples/text/src/buffer.rs | 23 +- examples/text/src/font/layout.rs | 8 +- examples/text/src/font/matches.rs | 19 +- examples/text/src/font/shape.rs | 66 +++- examples/text/src/main.rs | 52 ++-- gtk4/src/lib.rs | 6 +- src/widget/expander.rs | 283 +++++++++++++---- src/widget/header_bar.rs | 30 +- src/widget/icon.rs | 7 +- src/widget/list.rs | 7 +- src/widget/list_box.rs | 488 ++++++++++++++++++++++++++++++ src/widget/mod.rs | 3 + src/widget/nav.rs | 20 +- src/widget/navbar.rs | 162 +++++----- src/widget/scrollable.rs | 6 +- src/widget/toggler.rs | 5 +- 22 files changed, 1222 insertions(+), 738 deletions(-) create mode 100644 examples/cosmic/src/window.rs delete mode 100644 examples/settings/Cargo.toml delete mode 100644 examples/settings/src/main.rs delete mode 100644 examples/settings/src/window.rs create mode 100644 src/widget/list_box.rs diff --git a/.gitignore b/.gitignore index 96ef6c0..6a59f55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +/.idea \ No newline at end of file diff --git a/examples/cosmic/src/main.rs b/examples/cosmic/src/main.rs index b1cf307..87b7ef2 100644 --- a/examples/cosmic/src/main.rs +++ b/examples/cosmic/src/main.rs @@ -1,33 +1,7 @@ -use cosmic::{ - widget::{ - button, - list_item, - list_row, - list_section, - list_view, - nav_button, - toggler, - nav_bar_style, - header_bar, - }, - settings, - iced::{self, theme, Alignment, Application, Color, Command, Element, Length, Theme}, - iced::widget::{ - checkbox, - column, - container, - horizontal_space, - pick_list, - progress_bar, - radio, - row, - slider, - text - }, - iced_lazy::responsive, - iced_winit::window::{drag, maximize, minimize}, - scrollable -}; +use cosmic::{iced::Application, settings}; + +mod window; +pub use window::*; pub fn main() -> cosmic::iced::Result { let mut settings = settings(); @@ -36,267 +10,3 @@ pub fn main() -> cosmic::iced::Result { // settings.window.decorations = false; Window::run(settings) } - - -#[derive(Default)] -struct Window { - page: u8, - debug: bool, - theme: Theme, - slider_value: f32, - checkbox_value: bool, - toggler_value: bool, - pick_list_selected: Option<&'static str>, - sidebar_toggled: bool, - show_minimize: bool, - show_maximize: bool, - exit: bool, -} - -impl Window { - pub fn sidebar_toggled(mut self, toggled: bool) -> Self { - self.sidebar_toggled = toggled; - self - } - - pub fn show_maximize(mut self, show: bool) -> Self { - self.show_maximize = show; - self - } - - pub fn show_minimize(mut self, show: bool) -> Self { - self.show_minimize = show; - self - } -} - -#[allow(dead_code)] -#[derive(Clone, Copy, Debug)] -enum Message { - Page(u8), - Debug(bool), - ThemeChanged(Theme), - ButtonPressed, - SliderChanged(f32), - CheckboxToggled(bool), - TogglerToggled(bool), - PickListSelected(&'static str), - Close, - ToggleSidebar, - Drag, - Minimize, - Maximize, -} - -impl Application for Window { - type Executor = iced::executor::Default; - type Flags = (); - type Message = Message; - type Theme = Theme; - - fn new(_flags: ()) -> (Self, Command) { - let mut window = Window::default() - .sidebar_toggled(true) - .show_maximize(true) - .show_minimize(true); - window.slider_value = 50.0; - window.pick_list_selected = Some("Option 1"); - (window, Command::none()) - } - - fn title(&self) -> String { - String::from("COSMIC Design System - Iced") - } - - fn update(&mut self, message: Message) -> iced::Command { - match message { - Message::Page(page) => self.page = page, - Message::Debug(debug) => self.debug = debug, - Message::ThemeChanged(theme) => self.theme = theme, - Message::ButtonPressed => {} - Message::SliderChanged(value) => self.slider_value = value, - Message::CheckboxToggled(value) => self.checkbox_value = value, - Message::TogglerToggled(value) => self.toggler_value = value, - Message::PickListSelected(value) => self.pick_list_selected = Some(value), - Message::Close => self.exit = true, - Message::ToggleSidebar => self.sidebar_toggled = !self.sidebar_toggled, - Message::Drag => return drag(), - Message::Minimize => return minimize(), - Message::Maximize => return maximize(), - } - - iced::Command::none() - } - - fn view(&self) -> Element { - let mut header: Element = header_bar() - .title(self.title()) - .sidebar_active(self.sidebar_toggled) - .show_minimize(self.show_minimize) - .show_maximize(self.show_maximize) - .on_close(Message::Close) - .on_drag(Message::Drag) - .on_maximize(Message::Maximize) - .on_minimize(Message::Minimize) - .on_sidebar_toggle(Message::ToggleSidebar) - .into(); - - if self.debug { - header = header.explain(Color::WHITE); - } - - // TODO: Adding responsive makes this regenerate on every size change, and regeneration - // involves allocations for many different items. Ideally, we could only make the nav bar - // responsive and leave the content to be sized normally. - let content = responsive(|size| { - let condensed = size.width < 900.0; - - let sidebar: Element<_> = cosmic::navbar![ - nav_button!("network-wireless", "Wi-Fi", condensed) - .on_press(Message::Page(0)) - .style(if self.page == 0 { theme::Button::Primary } else { theme::Button::Text }) - , - nav_button!("preferences-desktop", "Desktop", condensed) - .on_press(Message::Page(1)) - .style(if self.page == 1 { theme::Button::Primary } else { theme::Button::Text }) - , - nav_button!("system-software-update", "OS Upgrade & Recovery", condensed) - .on_press(Message::Page(2)) - .style(if self.page == 2 { theme::Button::Primary } else { theme::Button::Text }), - ] - .active(self.sidebar_toggled) - .condensed(condensed) - .style(theme::Container::Custom(nav_bar_style)) - .into(); - - let choose_theme = [Theme::Light, Theme::Dark].iter().fold( - row![text("Debug theme:")].spacing(10).align_items(Alignment::Center), - |row, theme| { - row.push(radio( - format!("{:?}", theme), - *theme, - Some(self.theme), - Message::ThemeChanged, - )) - }, - ); - - let content: Element<_> = list_view!( - list_section!( - "Debug", - choose_theme, - toggler( - String::from("Debug layout"), - self.debug, - Message::Debug, - ) - ), - list_section!( - "Buttons", - list_row!( - button!("Primary") - .style(theme::Button::Primary) - .on_press(Message::ButtonPressed) - , - button!("Secondary") - .style(theme::Button::Secondary) - .on_press(Message::ButtonPressed) - , - button!("Positive") - .style(theme::Button::Positive) - .on_press(Message::ButtonPressed) - , - button!("Destructive") - .style(theme::Button::Destructive) - .on_press(Message::ButtonPressed) - , - button!("Text") - .style(theme::Button::Text) - .on_press(Message::ButtonPressed) - , - ), - list_row!( - button!("Primary") - .style(theme::Button::Primary) - , - button!("Secondary") - .style(theme::Button::Secondary) - , - button!("Positive") - .style(theme::Button::Positive) - , - button!("Destructive") - .style(theme::Button::Destructive) - , - button!("Text") - .style(theme::Button::Text) - , - ), - ), - list_section!( - "Controls", - list_item!( - "Toggler", - toggler(None, self.toggler_value, Message::TogglerToggled) - ), - list_item!( - "Pick List (TODO)", - pick_list( - vec![ - "Option 1", - "Option 2", - "Option 3", - "Option 4", - ], - self.pick_list_selected, - Message::PickListSelected - ) - .padding([8, 0, 8, 16]) - ), - list_item!( - "Slider", - slider(0.0..=100.0, self.slider_value, Message::SliderChanged) - .width(Length::Units(250)) - ), - list_item!( - "Progress", - progress_bar(0.0..=100.0, self.slider_value) - .width(Length::Units(250)) - .height(Length::Units(4)) - ), - checkbox("Checkbox", self.checkbox_value, Message::CheckboxToggled), - ) - ) - .into(); - - let mut widgets = Vec::with_capacity(2); - - widgets.push(if self.debug { sidebar.explain(Color::WHITE) } else { sidebar }); - - widgets.push( - scrollable!(row![ - horizontal_space(Length::Fill), - if self.debug { content.explain(Color::WHITE) } else { content }, - horizontal_space(Length::Fill), - ]) - .into() - ); - - container(row(widgets)) - .padding([16, 16]) - .width(Length::Fill) - .height(Length::Fill) - .into() - }).into(); - - column(vec![header, content]).into() - } - - fn should_exit(&self) -> bool { - self.exit - } - - fn theme(&self) -> Theme { - self.theme - } -} diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs new file mode 100644 index 0000000..5b40ef4 --- /dev/null +++ b/examples/cosmic/src/window.rs @@ -0,0 +1,307 @@ +use cosmic::widget::{expander, expander_row, list_section_style, ListBox}; +use cosmic::{ + iced::widget::{ + checkbox, column, container, horizontal_space, pick_list, progress_bar, radio, row, slider, + text, + }, + iced::{self, theme, Alignment, Application, Color, Command, Element, Length, Theme}, + iced_lazy::responsive, + iced_winit::window::{drag, maximize, minimize}, + scrollable, + widget::{ + button, header_bar, list_item, list_row, list_section, list_view, nav_bar_style, + nav_button, toggler, + }, +}; + +#[derive(Default)] +pub struct Window { + page: u8, + debug: bool, + theme: Theme, + slider_value: f32, + checkbox_value: bool, + toggler_value: bool, + pick_list_selected: Option<&'static str>, + sidebar_toggled: bool, + show_minimize: bool, + show_maximize: bool, + exit: bool, +} + +impl Window { + pub fn sidebar_toggled(mut self, toggled: bool) -> Self { + self.sidebar_toggled = toggled; + self + } + + pub fn show_maximize(mut self, show: bool) -> Self { + self.show_maximize = show; + self + } + + pub fn show_minimize(mut self, show: bool) -> Self { + self.show_minimize = show; + self + } +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug)] +pub enum Message { + Page(u8), + Debug(bool), + ThemeChanged(Theme), + ButtonPressed, + SliderChanged(f32), + CheckboxToggled(bool), + TogglerToggled(bool), + PickListSelected(&'static str), + RowSelected(usize), + Close, + ToggleSidebar, + Drag, + Minimize, + Maximize, +} + +impl Application for Window { + type Executor = iced::executor::Default; + type Flags = (); + type Message = Message; + type Theme = Theme; + + fn new(_flags: ()) -> (Self, Command) { + let mut window = Window::default() + .sidebar_toggled(true) + .show_maximize(true) + .show_minimize(true); + window.slider_value = 50.0; + window.pick_list_selected = Some("Option 1"); + (window, Command::none()) + } + + fn title(&self) -> String { + String::from("COSMIC Design System - Iced") + } + + fn update(&mut self, message: Message) -> iced::Command { + match message { + Message::Page(page) => self.page = page, + Message::Debug(debug) => self.debug = debug, + Message::ThemeChanged(theme) => self.theme = theme, + Message::ButtonPressed => {} + Message::SliderChanged(value) => self.slider_value = value, + Message::CheckboxToggled(value) => self.checkbox_value = value, + Message::TogglerToggled(value) => self.toggler_value = value, + Message::PickListSelected(value) => self.pick_list_selected = Some(value), + Message::Close => self.exit = true, + Message::ToggleSidebar => self.sidebar_toggled = !self.sidebar_toggled, + Message::Drag => return drag(), + Message::Minimize => return minimize(), + Message::Maximize => return maximize(), + Message::RowSelected(row) => println!("Selected row {row}"), + } + + iced::Command::none() + } + + fn view(&self) -> Element { + let mut header: Element = header_bar() + .title(self.title()) + .sidebar_active(self.sidebar_toggled) + .show_minimize(self.show_minimize) + .show_maximize(self.show_maximize) + .on_close(Message::Close) + .on_drag(Message::Drag) + .on_maximize(Message::Maximize) + .on_minimize(Message::Minimize) + .on_sidebar_toggle(Message::ToggleSidebar) + .into(); + + if self.debug { + header = header.explain(Color::WHITE); + } + + // TODO: Adding responsive makes this regenerate on every size change, and regeneration + // involves allocations for many different items. Ideally, we could only make the nav bar + // responsive and leave the content to be sized normally. + let content = responsive(|size| { + let condensed = size.width < 900.0; + + let sidebar: Element<_> = cosmic::navbar![ + nav_button!("network-wireless", "Wi-Fi", condensed) + .on_press(Message::Page(0)) + .style(if self.page == 0 { + theme::Button::Primary + } else { + theme::Button::Text + }), + nav_button!("preferences-desktop", "Desktop", condensed) + .on_press(Message::Page(1)) + .style(if self.page == 1 { + theme::Button::Primary + } else { + theme::Button::Text + }), + nav_button!("system-software-update", "OS Upgrade & Recovery", condensed) + .on_press(Message::Page(2)) + .style(if self.page == 2 { + theme::Button::Primary + } else { + theme::Button::Text + }), + ] + .active(self.sidebar_toggled) + .condensed(condensed) + .style(theme::Container::Custom(nav_bar_style)) + .into(); + + let choose_theme = [Theme::Light, Theme::Dark].iter().fold( + row![text("Debug theme:")] + .spacing(10) + .align_items(Alignment::Center), + |row, theme| { + row.push(radio( + format!("{:?}", theme), + *theme, + Some(self.theme), + Message::ThemeChanged, + )) + }, + ); + + let content: Element<_> = list_view!( + list_section!( + "Debug", + choose_theme, + toggler(String::from("Debug layout"), self.debug, Message::Debug,) + ), + list_section!( + "Buttons", + list_row!( + button!("Primary") + .style(theme::Button::Primary) + .on_press(Message::ButtonPressed), + button!("Secondary") + .style(theme::Button::Secondary) + .on_press(Message::ButtonPressed), + button!("Positive") + .style(theme::Button::Positive) + .on_press(Message::ButtonPressed), + button!("Destructive") + .style(theme::Button::Destructive) + .on_press(Message::ButtonPressed), + button!("Text") + .style(theme::Button::Text) + .on_press(Message::ButtonPressed), + ), + list_row!( + button!("Primary").style(theme::Button::Primary), + button!("Secondary").style(theme::Button::Secondary), + button!("Positive").style(theme::Button::Positive), + button!("Destructive").style(theme::Button::Destructive), + button!("Text").style(theme::Button::Text), + ), + ), + list_section!( + "Controls", + list_item!( + "Toggler", + toggler(None, self.toggler_value, Message::TogglerToggled) + ), + list_item!( + "Pick List (TODO)", + pick_list( + vec!["Option 1", "Option 2", "Option 3", "Option 4",], + self.pick_list_selected, + Message::PickListSelected + ) + .padding([8, 0, 8, 16]) + ), + list_item!( + "Slider", + slider(0.0..=100.0, self.slider_value, Message::SliderChanged) + .width(Length::Units(250)) + ), + list_item!( + "Progress", + progress_bar(0.0..=100.0, self.slider_value) + .width(Length::Units(250)) + .height(Length::Units(4)) + ), + checkbox("Checkbox", self.checkbox_value, Message::CheckboxToggled), + ), + list_section!( + "Expander", + expander() + .title("Label") + .subtitle("Caption") + .icon(String::from("edit-paste")) + .on_row_selected(Box::new(Message::RowSelected)) + .rows(vec![ + expander_row() + .title("Label") + .subtitle("Caption") + .icon(String::from("help-about")), + expander_row().subtitle("Caption").title("Label"), + expander_row().title("Label") + ]) + ), + list_section!( + "List Box", + ListBox::with_children( + vec![ + cosmic::list_box_row!("Title").into(), + cosmic::list_box_row!("Title", "Subtitle").into(), + cosmic::list_box_row!("Title", "", "edit-paste").into(), + cosmic::list_box_row!("", "Subtitle", "edit-paste").into(), + cosmic::list_box_row!("Title", "Subtitle", "edit-paste").into() + ], + true, + ) + .style(theme::Container::Custom(list_section_style)) + ), + ) + .into(); + + let mut widgets = Vec::with_capacity(2); + + widgets.push(if self.debug { + sidebar.explain(Color::WHITE) + } else { + sidebar + }); + + widgets.push( + scrollable!(row![ + horizontal_space(Length::Fill), + if self.debug { + content.explain(Color::WHITE) + } else { + content + }, + horizontal_space(Length::Fill), + ]) + .into(), + ); + + container(row(widgets)) + .padding([16, 16]) + .width(Length::Fill) + .height(Length::Fill) + .into() + }) + .into(); + + column(vec![header, content]).into() + } + + fn should_exit(&self) -> bool { + self.exit + } + + fn theme(&self) -> Theme { + self.theme + } +} diff --git a/examples/settings/Cargo.toml b/examples/settings/Cargo.toml deleted file mode 100644 index 7e6fe77..0000000 --- a/examples/settings/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "settings" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -libcosmic = { path = "../..", features = ["debug"] } \ No newline at end of file diff --git a/examples/settings/src/main.rs b/examples/settings/src/main.rs deleted file mode 100644 index 6543540..0000000 --- a/examples/settings/src/main.rs +++ /dev/null @@ -1,10 +0,0 @@ -use cosmic::{settings, iced::Application}; -mod window; -use window::*; - -fn main() -> cosmic::iced::Result { - let mut settings = settings(); - settings.window.min_size = Some((600, 300)); - - App::run(settings) -} diff --git a/examples/settings/src/window.rs b/examples/settings/src/window.rs deleted file mode 100644 index 7302b93..0000000 --- a/examples/settings/src/window.rs +++ /dev/null @@ -1,150 +0,0 @@ -use cosmic::{ - widget::{ - header_bar, - nav_bar_style - }, - iced, - iced::{ - Theme, - Application, - Element, - widget::{ - container, - column - }, - }, - iced_winit::{ - Command, - Length, - widget::row, - window::drag, - theme - }, - nav_button, - iced_lazy::responsive -}; - -#[derive(Default)] -pub struct App { - page: u8, - theme: Theme, - sidebar_toggled: bool, - show_minimize: bool, - show_maximize: bool, - exit: bool, -} - -impl App { - pub fn sidebar_toggled(mut self, toggled: bool) -> Self { - self.sidebar_toggled = toggled; - self - } - - pub fn show_maximize(mut self, show: bool) -> Self { - self.show_maximize = show; - self - } - - pub fn show_minimize(mut self, show: bool) -> Self { - self.show_minimize = show; - self - } -} - -#[allow(dead_code)] -#[derive(Debug, Clone, Copy)] -pub enum AppMsg { - Close, - ToggleSidebar(bool), - Drag, - Minimize, - Maximize, - Page(u8), -} - -impl Application for App { - type Executor = iced::executor::Default; - type Flags = (); - type Message = AppMsg; - type Theme = Theme; - - fn new(_flags: Self::Flags) -> (Self, Command) { - ( - App::default() - .sidebar_toggled(true) - .show_maximize(true) - .show_minimize(true), - Command::none() - ) - } - - fn title(&self) -> String { - String::from("COSMIC Settings") - } - - fn update(&mut self, message: Self::Message) -> Command { - match message { - AppMsg::Close => self.exit = true, - AppMsg::ToggleSidebar(toggled) => self.sidebar_toggled = toggled, - AppMsg::Drag => return drag(), - AppMsg::Minimize => {}, - AppMsg::Maximize => {}, - AppMsg::Page(page) => self.page = page, - } - Command::none() - } - - fn view(&self) -> iced::Element { - let header = header_bar( - self.title().as_str(), - self.sidebar_toggled, - self.show_minimize, - self.show_maximize, - |toggled| AppMsg::ToggleSidebar(toggled), - || AppMsg::Close, - || AppMsg::Drag - ).into(); - - let content = responsive(|size| { - let condensed = size.width < 900.0; - - let sidebar: Element<_> = cosmic::navbar![ - nav_button!("network-wireless", "Wi-Fi", condensed) - .on_press(AppMsg::Page(0)) - .style(if self.page == 0 { theme::Button::Primary } else { theme::Button::Text }) - , - nav_button!("preferences-desktop", "Desktop", condensed) - .on_press(AppMsg::Page(1)) - .style(if self.page == 1 { theme::Button::Primary } else { theme::Button::Text }) - , - nav_button!("system-software-update", "OS Upgrade & Recovery", condensed) - .on_press(AppMsg::Page(2)) - .style(if self.page == 2 { theme::Button::Primary } else { theme::Button::Text }), - ] - .active(self.sidebar_toggled) - .condensed(condensed) - .style(theme::Container::Custom(nav_bar_style)) - .into(); - - let mut widgets = Vec::with_capacity(2); - - widgets.push(sidebar); - - container(row(widgets)) - .padding([8, 8]) - .width(Length::Fill) - .height(Length::Fill) - .into() - }).into(); - - column(vec![header, content]).into() - } - - fn should_exit(&self) -> bool { - self.exit - } - - fn theme(&self) -> Theme { - self.theme - } -} \ No newline at end of file diff --git a/examples/text/src/buffer.rs b/examples/text/src/buffer.rs index fe9e5f1..97d6c11 100644 --- a/examples/text/src/buffer.rs +++ b/examples/text/src/buffer.rs @@ -93,7 +93,12 @@ impl<'a> TextBuffer<'a> { self.layout_lines.clear(); for line in self.shape_lines.iter() { let layout_i = self.layout_lines.len(); - line.layout(self.font_size, self.line_width, &mut self.layout_lines, layout_i); + line.layout( + self.font_size, + self.line_width, + &mut self.layout_lines, + layout_i, + ); } self.redraw = true; @@ -173,7 +178,7 @@ impl<'a> TextBuffer<'a> { self.cursor.glyph -= 1; self.redraw = true; } - }, + } TextAction::Right => { let line = &self.layout_lines[self.cursor.line]; if self.cursor.glyph > line.glyphs.len() { @@ -184,19 +189,19 @@ impl<'a> TextBuffer<'a> { self.cursor.glyph += 1; self.redraw = true; } - }, + } TextAction::Up => { if self.cursor.line > 0 { self.cursor.line -= 1; self.redraw = true; } - }, + } TextAction::Down => { if self.cursor.line + 1 < self.layout_lines.len() { self.cursor.line += 1; self.redraw = true; } - }, + } TextAction::Backspace => { let line = &self.layout_lines[self.cursor.line]; if self.cursor.glyph > line.glyphs.len() { @@ -210,7 +215,7 @@ impl<'a> TextBuffer<'a> { text_line.remove(glyph.start); self.reshape_line(line.line_i); } - }, + } TextAction::Delete => { let line = &self.layout_lines[self.cursor.line]; if self.cursor.glyph < line.glyphs.len() { @@ -219,7 +224,7 @@ impl<'a> TextBuffer<'a> { text_line.remove(glyph.start); self.reshape_line(line.line_i); } - }, + } TextAction::Insert(character) => { let line = &self.layout_lines[self.cursor.line]; if self.cursor.glyph >= line.glyphs.len() { @@ -229,7 +234,7 @@ impl<'a> TextBuffer<'a> { text_line.insert(glyph.end, character); self.cursor.glyph += 1; self.reshape_line(line.line_i); - }, + } None => { let text_line = &mut self.text_lines[line.line_i.get()]; text_line.push(character); @@ -244,7 +249,7 @@ impl<'a> TextBuffer<'a> { self.cursor.glyph += 1; self.reshape_line(line.line_i); } - }, + } } } } diff --git a/examples/text/src/font/layout.rs b/examples/text/src/font/layout.rs index 31210fb..2ad4462 100644 --- a/examples/text/src/font/layout.rs +++ b/examples/text/src/font/layout.rs @@ -29,9 +29,7 @@ impl<'a> FontLayoutLine<'a> { let y = bb.min.y as i32; outline.draw(|off_x, off_y, v| { //TODO: ensure v * 255.0 does not overflow! - let color = - ((v * 255.0) as u32) << 24 | - base & 0xFFFFFF; + let color = ((v * 255.0) as u32) << 24 | base & 0xFFFFFF; f(x + off_x as i32, y + off_y as i32, color); }); } @@ -42,9 +40,7 @@ impl<'a> FontLayoutLine<'a> { let y = bb.min.y; glyph.inner.draw(|off_x, off_y, v| { //TODO: ensure v * 255.0 does not overflow! - let color = - ((v * 255.0) as u32) << 24 | - base & 0xFFFFFF; + let color = ((v * 255.0) as u32) << 24 | base & 0xFFFFFF; f(x + off_x as i32, y + off_y as i32, color); }); } diff --git a/examples/text/src/font/matches.rs b/examples/text/src/font/matches.rs index dd9ec46..028cacb 100644 --- a/examples/text/src/font/matches.rs +++ b/examples/text/src/font/matches.rs @@ -1,4 +1,4 @@ -use super::{Font, FontLineIndex, FontShapeGlyph, FontShapeWord, FontShapeLine, FontShapeSpan}; +use super::{Font, FontLineIndex, FontShapeGlyph, FontShapeLine, FontShapeSpan, FontShapeWord}; pub struct FontMatches<'a> { pub fonts: Vec>, @@ -22,7 +22,7 @@ impl<'a> FontMatches<'a> { let rtl = match buffer.direction() { rustybuzz::Direction::RightToLeft => true, //TODO: other directions? - _ => false, + _ => false, }; assert_eq!(rtl, span_rtl); @@ -177,7 +177,14 @@ impl<'a> FontMatches<'a> { FontShapeWord { blank, glyphs } } - fn shape_span(&self, line: &str, start_span: usize, end_span: usize, line_rtl: bool, span_rtl: bool) -> FontShapeSpan { + fn shape_span( + &self, + line: &str, + start_span: usize, + end_span: usize, + line_rtl: bool, + span_rtl: bool, + ) -> FontShapeSpan { let span = &line[start_span..end_span]; log::debug!(" Span {}: '{}'", if span_rtl { "RTL" } else { "LTR" }, span); @@ -251,10 +258,6 @@ impl<'a> FontMatches<'a> { line_rtl }; - FontShapeLine { - line_i, - rtl, - spans, - } + FontShapeLine { line_i, rtl, spans } } } diff --git a/examples/text/src/font/shape.rs b/examples/text/src/font/shape.rs index cc3fe2a..f744892 100644 --- a/examples/text/src/font/shape.rs +++ b/examples/text/src/font/shape.rs @@ -39,10 +39,7 @@ impl<'a> FontShapeGlyph<'a> { #[cfg(feature = "rusttype")] let inner = self.font.rusttype.glyph(self.inner) .scaled(rusttype::Scale::uniform(font_size as f32)) - .positioned(rusttype::point( - x + x_offset, - y - y_offset, - )); + .positioned(rusttype::point(x + x_offset, y - y_offset)); #[cfg(feature = "swash")] let inner = CacheKey::new( @@ -79,7 +76,13 @@ pub struct FontShapeLine<'a> { } impl<'a> FontShapeLine<'a> { - pub fn layout(&self, font_size: i32, line_width: i32, layout_lines: &mut Vec>, mut layout_i: usize) { + pub fn layout( + &self, + font_size: i32, + line_width: i32, + layout_lines: &mut Vec>, + mut layout_i: usize, + ) { let mut push_line = true; let mut glyphs = Vec::new(); @@ -128,7 +131,7 @@ impl<'a> FontShapeLine<'a> { fit_x += word_size; } } - if ! word_ranges.is_empty() { + if !word_ranges.is_empty() { while fitting_end > 0 { if span.words[fitting_end - 1].blank { fitting_end -= 1; @@ -174,6 +177,33 @@ impl<'a> FontShapeLine<'a> { for (range, wrap) in word_ranges { for word in span.words[range].iter() { + let mut word_size = 0.0; + for glyph in word.glyphs.iter() { + word_size += font_size as f32 * glyph.x_advance; + } + + //TODO: make wrapping optional + let wrap = if self.rtl { + x - word_size < end_x + } else { + x + word_size > end_x + }; + if wrap && !glyphs.is_empty() { + let mut glyphs_swap = Vec::new(); + std::mem::swap(&mut glyphs, &mut glyphs_swap); + layout_lines.insert( + layout_i, + FontLayoutLine { + line_i: self.line_i, + glyphs: glyphs_swap, + }, + ); + layout_i += 1; + + x = start_x; + y = 0.0; + } + for glyph in word.glyphs.iter() { let x_advance = font_size as f32 * glyph.x_advance; let y_advance = font_size as f32 * glyph.y_advance; @@ -185,7 +215,7 @@ impl<'a> FontShapeLine<'a> { glyphs.push(glyph.layout(font_size, x, y)); push_line = true; - if ! self.rtl { + if !self.rtl { x += x_advance; } y += y_advance; @@ -195,10 +225,13 @@ impl<'a> FontShapeLine<'a> { if wrap { let mut glyphs_swap = Vec::new(); std::mem::swap(&mut glyphs, &mut glyphs_swap); - layout_lines.insert(layout_i, FontLayoutLine { - line_i: self.line_i, - glyphs: glyphs_swap - }); + layout_lines.insert( + layout_i, + FontLayoutLine { + line_i: self.line_i, + glyphs: glyphs_swap, + }, + ); layout_i += 1; x = start_x; @@ -208,10 +241,13 @@ impl<'a> FontShapeLine<'a> { } if push_line { - layout_lines.insert(layout_i, FontLayoutLine { - line_i: self.line_i, - glyphs - }); + layout_lines.insert( + layout_i, + FontLayoutLine { + line_i: self.line_i, + glyphs, + }, + ); } } } diff --git a/examples/text/src/main.rs b/examples/text/src/main.rs index db7b5ea..b73cb1e 100644 --- a/examples/text/src/main.rs +++ b/examples/text/src/main.rs @@ -18,7 +18,7 @@ fn main() { Ok((w, h)) => { eprintln!("Display size: {}, {}", w, h); (h as i32 / 1600) + 1 - }, + } Err(err) => { eprintln!("Failed to get display size: {}", err); 1 @@ -31,8 +31,9 @@ fn main() { 1024 * display_scale as u32, 768 * display_scale as u32, "COSMIC TEXT", - &[WindowFlag::Resizable] - ).unwrap(); + &[WindowFlag::Resizable], + ) + .unwrap(); let font_system = FontSystem::new(); @@ -122,22 +123,27 @@ fn main() { let mut new_cursor_opt = None; let mut line_y = line_height; - for (line_i, line) in buffer.layout_lines().iter().skip(scroll as usize).enumerate() { + for (line_i, line) in buffer + .layout_lines() + .iter() + .skip(scroll as usize) + .enumerate() + { if line_y >= window.height() as i32 { break; } if mouse_left - && mouse_y >= line_y - font_size - && mouse_y < line_y - font_size + line_height + && mouse_y >= line_y - font_size + && mouse_y < line_y - font_size + line_height { let new_cursor_line = line_i + scroll as usize; let mut new_cursor_glyph = line.glyphs.len(); for (glyph_i, glyph) in line.glyphs.iter().enumerate() { if mouse_x >= line_x + glyph.x as i32 - && mouse_x <= line_x + (glyph.x + glyph.w) as i32 + && mouse_x <= line_x + (glyph.x + glyph.w) as i32 { - new_cursor_glyph = glyph_i; + new_cursor_glyph = glyph_i; } } new_cursor_opt = Some(TextCursor::new(new_cursor_line, new_cursor_glyph)); @@ -181,14 +187,14 @@ fn main() { if buffer.cursor.glyph >= line.glyphs.len() { let x = match line.glyphs.last() { Some(glyph) => glyph.x + glyph.w, - None => 0.0 + None => 0.0, }; window.rect( line_x + x as i32, line_y - font_size, (font_size / 2) as u32, line_height as u32, - Color::rgba(0xFF, 0xFF, 0xFF, 0x20) + Color::rgba(0xFF, 0xFF, 0xFF, 0x20), ); } else { let glyph = &line.glyphs[buffer.cursor.glyph]; @@ -197,7 +203,7 @@ fn main() { line_y - font_size, glyph.w as u32, line_height as u32, - Color::rgba(0xFF, 0xFF, 0xFF, 0x20) + Color::rgba(0xFF, 0xFF, 0xFF, 0x20), ); let text_line = &buffer.text_lines()[line.line_i.get()]; @@ -269,24 +275,26 @@ fn main() { orbclient::K_MINUS if event.pressed && ctrl_pressed => if font_size_i > 0 { font_size_i -= 1; buffer.set_font_size(font_sizes[font_size_i].0 * display_scale); - }, - orbclient::K_EQUALS if event.pressed && ctrl_pressed => if font_size_i + 1 < font_sizes.len() { - font_size_i += 1; - buffer.set_font_size(font_sizes[font_size_i].0 * display_scale); - }, - _ => (), + } + orbclient::K_EQUALS if event.pressed && ctrl_pressed => { + if font_size_i + 1 < font_sizes.len() { + font_size_i += 1; + buffer.set_font_size(font_sizes[font_size_i].0 * display_scale); + } + } + _ => (), } }, - EventOption::TextInput(event) if ! ctrl_pressed => { + EventOption::TextInput(event) if !ctrl_pressed => { buffer.action(TextAction::Insert(event.character)); - }, + } EventOption::Mouse(event) => { mouse_x = event.x; mouse_y = event.y; if mouse_left { rehit = true; } - }, + } EventOption::Button(event) => { if event.left != mouse_left { mouse_left = event.left; @@ -297,11 +305,11 @@ fn main() { } EventOption::Resize(event) => { buffer.set_line_width(event.width as i32 - line_x * 2); - }, + } EventOption::Scroll(event) => { scroll -= event.y * 3; buffer.redraw = true; - }, + } EventOption::Quit(_) => return, _ => (), } diff --git a/gtk4/src/lib.rs b/gtk4/src/lib.rs index 9a7dc04..51598de 100644 --- a/gtk4/src/lib.rs +++ b/gtk4/src/lib.rs @@ -68,9 +68,9 @@ pub fn init() -> (Option, Option) { monitor }); let path = xdg::BaseDirectories::with_prefix("gtk-4.0") - .ok() - .and_then(|xdg_dirs| xdg_dirs.find_config_file("cosmic.css")) - .unwrap_or_else(|| "~/.config/gtk-4.0/cosmic.css".into()); + .ok() + .and_then(|xdg_dirs| xdg_dirs.find_config_file("cosmic.css")) + .unwrap_or_else(|| "~/.config/gtk-4.0/cosmic.css".into()); let cosmic_file = gio::File::for_path(path); cosmic_user_provider.load_from_file(&cosmic_file); diff --git a/src/widget/expander.rs b/src/widget/expander.rs index 450b8d1..3219312 100644 --- a/src/widget/expander.rs +++ b/src/widget/expander.rs @@ -1,81 +1,232 @@ use std::vec; +use crate::list_box_row; use apply::Apply; +use derive_setters::Setters; use iced::{ - Element, - Length, - widget::{ - row, - horizontal_space, button, container, text, Column - }, alignment::{Vertical, Horizontal}, theme + theme, + widget::{self, button, container, horizontal_space, row, text, Column}, + Alignment, Background, Element, Length, Renderer, Theme, }; -use iced_native::widget::column; +use iced_lazy::Component; +use iced_native::widget::{column, event_container, horizontal_rule}; -#[derive(Default)] -pub struct Expander -{ - pub expanded: bool +#[derive(Setters)] +pub struct Expander<'a, Message> { + title: &'a str, + #[setters(strip_option)] + subtitle: Option<&'a str>, + #[setters(strip_option)] + icon: Option, + expansible: bool, + #[setters(skip)] + rows: Option>>, + #[setters(strip_option)] + on_row_selected: Option Message + 'a>>, +} + +pub fn expander<'a, Message>() -> Expander<'a, Message> { + Expander { + title: "", + subtitle: None, + icon: None, + expansible: false, + rows: None, + on_row_selected: None, + } +} + +#[derive(Setters, Default, Debug, Clone)] +pub struct ExpanderRow<'a> { + pub(crate) title: &'a str, + #[setters(strip_option)] + pub subtitle: Option<&'a str>, + #[setters(strip_option)] + pub icon: Option, +} + +pub fn expander_row<'a>() -> ExpanderRow<'a> { + ExpanderRow { + title: "", + subtitle: None, + icon: None, + } +} + +pub struct ExpanderState { + pub expanded: bool, +} + +impl Default for ExpanderState { + fn default() -> Self { + Self { expanded: true } + } } #[derive(Clone, Copy, Debug)] -pub enum ExpanderMsg { +pub enum ExpanderEvent { Expand, + RowSelected(usize), } -impl Expander { - pub fn new() -> Self { - Self::default() +impl<'a, Message> Expander<'a, Message> { + pub fn rows(mut self, rows: Vec>) -> Self { + self.rows = Some(rows); + self.expansible = true; + self } - pub fn render<'a, T>(&self, children: Vec>) -> Element<'a, T> - where T: Clone + From + 'static - { - let title = text("Title") - .size(18) - .vertical_alignment(Vertical::Center) - .horizontal_alignment(Horizontal::Center) - .into(); - let subtitle = iced::widget::text("Subtitle") - .size(14) - .vertical_alignment(Vertical::Center) - .horizontal_alignment(Horizontal::Center) - .into(); - let header = column( - vec![title, subtitle] - ).into(); - let space = horizontal_space(Length::Fill).into(); - let icon = super::icon( - if self.expanded { - "go-down-symbolic" - } else { - "go-next-symbolic" - }, - 16 - ) - .apply(button) - .on_press(T::from(ExpanderMsg::Expand)) - .width(Length::Units(25)) - .into(); - - container( - column( - if self.expanded { - vec![ - row(vec![header, space, icon]).into(), - container( - Column::with_children(children) - ) - .style(theme::Container::Transparent) - .padding(5) - .into() - ] - } else { - vec![row(vec![header, space, icon]).into()] - } - ) - .padding(5) - ) - .style(theme::Container::Box) - .into() + pub fn push(&mut self, row: ExpanderRow<'a>) { + if self.rows.is_none() { + self.rows = Some(vec![]) + } + self.rows.as_mut().unwrap().push(row); } -} \ No newline at end of file +} + +impl<'a, Message: Clone + 'a> Component for Expander<'a, Message> { + type State = ExpanderState; + + type Event = ExpanderEvent; + + fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option { + match event { + ExpanderEvent::Expand => { + state.expanded = !state.expanded; + None + } + ExpanderEvent::RowSelected(index) => self + .on_row_selected + .as_ref() + .map(|on_row_selected| (on_row_selected)(index)), + } + } + + fn view(&self, state: &Self::State) -> Element { + let heading: Element = { + let mut captions = vec![text(&self.title).size(18).into()]; + if let Some(subtitle) = &self.subtitle { + captions.push(text(subtitle).size(16).into()); + } + let text = column(captions); + let space: Element = horizontal_space(Length::Fill).into(); + let toggler: Element = { + let mut icon = super::icon( + if state.expanded { + "go-down-symbolic" + } else { + "go-next-symbolic" + }, + 16, + ) + .apply(button) + .width(Length::Units(25)); + if self.expansible { + icon = icon.on_press(ExpanderEvent::Expand); + } + icon.into() + }; + + let items = if let Some(icon) = &self.icon { + let icon = super::icon(icon.as_str(), 20) + .apply(event_container) + .padding(10); + row![icon, text, space, toggler] + } else { + row![text, space, toggler] + }; + + container(items.align_items(Alignment::Center)) + .style(theme::Container::Custom(expander_heading_style)) + .padding(10) + .into() + }; + + let rows: Vec> = if let Some(rows) = &self.rows { + rows.iter() + .enumerate() + .map(|(index, row)| { + let subtitle = row.subtitle.unwrap_or_default(); + if let Some(icon) = &row.icon { + list_box_row!(row.title, subtitle, icon.as_str()) + .apply(event_container) + .on_press(ExpanderEvent::RowSelected(index)) + .into() + } else { + list_box_row!(row.title, subtitle) + .apply(event_container) + .on_press(ExpanderEvent::RowSelected(index)) + .into() + } + }) + .enumerate() + .flat_map(|(index, child)| { + if index != rows.len() - 1 { + vec![ + child, + horizontal_rule(1) + .style(theme::Rule::Custom(separator_style)) + .into(), + ] + } else { + vec![child] + } + }) + .collect() + } else { + vec![] + }; + + let rows: Element = Column::with_children(rows).into(); + + let mut layout = vec![heading]; + if state.expanded && self.expansible { + layout.push(rows) + } + + column(layout) + .apply(widget::container) + .height(Length::Shrink) + .style(theme::Container::Custom(expander_row_style)) + .into() + } +} + +impl<'a, Message: Clone + 'a> From> for Element<'a, Message> { + fn from(expander: Expander<'a, Message>) -> Self { + iced_lazy::component(expander) + } +} + +pub fn expander_heading_style(theme: &Theme) -> widget::container::Appearance { + let primary = &theme.cosmic().primary; + let accent = &theme.cosmic().accent; + widget::container::Appearance { + text_color: Some(accent.base.into()), + background: Some(Background::Color(primary.divider.into())), + border_radius: 8.0, + border_width: 0.0, + border_color: primary.on.into(), + } +} + +pub fn expander_row_style(theme: &Theme) -> widget::container::Appearance { + let cosmic = &theme.cosmic().primary; + widget::container::Appearance { + text_color: Some(cosmic.on.into()), + background: Some(Background::Color(cosmic.base.into())), + border_radius: 8.0, + border_width: 0.4, + border_color: cosmic.divider.into(), + } +} + +pub fn separator_style(theme: &Theme) -> widget::rule::Appearance { + let cosmic = &theme.cosmic().primary; + widget::rule::Appearance { + color: cosmic.divider.into(), + width: 1, + radius: 0.0, + fill_mode: widget::rule::FillMode::Padded(10), + } +} diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 6a43478..3b324e1 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -33,7 +33,7 @@ pub fn header_bar() -> HeaderBar { on_close: None, on_drag: None, on_maximize: None, - on_minimize: None + on_minimize: None, } } @@ -53,25 +53,15 @@ impl Component for HeaderBar { fn update(&mut self, _state: &mut Self::State, event: Self::Event) -> Option { match event { - HeaderEvent::Close => { - self.on_close.clone() - } - - HeaderEvent::ToggleSidebar => { - self.on_sidebar_toggle.clone() - } - - HeaderEvent::Drag => { - self.on_drag.clone() - } - - HeaderEvent::Maximize => { - self.on_maximize.clone() - } - - HeaderEvent::Minimize => { - self.on_minimize.clone() - } + HeaderEvent::Close => self.on_close.clone(), + + HeaderEvent::ToggleSidebar => self.on_sidebar_toggle.clone(), + + HeaderEvent::Drag => self.on_drag.clone(), + + HeaderEvent::Maximize => self.on_maximize.clone(), + + HeaderEvent::Minimize => self.on_minimize.clone(), } } diff --git a/src/widget/icon.rs b/src/widget/icon.rs index 0539c12..8faa243 100644 --- a/src/widget/icon.rs +++ b/src/widget/icon.rs @@ -1,7 +1,4 @@ -use iced::{ - Length, - widget::svg, -}; +use iced::{widget::svg, Length}; pub fn icon(name: &str, size: u16) -> svg::Svg { let handle = match freedesktop_icons::lookup(name) @@ -15,7 +12,7 @@ pub fn icon(name: &str, size: u16) -> svg::Svg { None => { eprintln!("icon '{}' size {} not found", name, size); svg::Handle::from_memory(Vec::new()) - }, + } }; svg::Svg::new(handle) .width(Length::Units(size)) diff --git a/src/widget/list.rs b/src/widget/list.rs index c2a6d2a..7977f6d 100644 --- a/src/widget/list.rs +++ b/src/widget/list.rs @@ -1,9 +1,4 @@ -use iced::{ - Background, - Color, - Theme, - widget, -}; +use iced::{widget, Background, Color, Theme}; #[macro_export] macro_rules! list_item { diff --git a/src/widget/list_box.rs b/src/widget/list_box.rs new file mode 100644 index 0000000..cf20d25 --- /dev/null +++ b/src/widget/list_box.rs @@ -0,0 +1,488 @@ +use derive_setters::Setters; +use iced::mouse::Interaction; +use iced::{overlay, Alignment, Length, Padding, Point, Rectangle}; +use iced_native::event::Status; +use iced_native::layout::flex::{resolve, Axis}; +use iced_native::layout::{Limits, Node}; +use iced_native::overlay::from_children; +use iced_native::renderer::Style; +use iced_native::widget::{column, horizontal_rule, Operation, Tree}; +use iced_native::{row, Clipboard, Element, Event, Layout, Shell, Widget, renderer, Background, Color}; +use iced_style::container::{Appearance, StyleSheet}; +use iced_style::theme; +use iced_style::theme::Container; + +#[derive(Setters)] +pub struct ListBox<'a, Message, Renderer> + where + Renderer: iced_native::Renderer, + Renderer::Theme: StyleSheet + iced_style::rule::StyleSheet, + ::Theme: iced_style::rule::StyleSheet +{ + spacing: u16, + padding: Padding, + width: Length, + height: Length, + max_width: u32, + align_items: Alignment, + style: ::Style, + children: Vec>, + #[setters(strip_option)] + placeholder: Option>, + on_item_selected: Option Message + 'a>>, +} + +impl<'a, Message: 'a, Renderer: iced_native::Renderer + 'a> ListBox<'a, Message, Renderer> +where + Renderer::Theme: StyleSheet + iced_style::rule::StyleSheet, + <::Theme as StyleSheet>::Style: From, + <::Theme as iced_style::rule::StyleSheet>::Style: From +{ + /// The default padding of a [`ListBox`] drawn by this renderer. + pub const DEFAULT_PADDING: u16 = 0; + + /// Creates an empty [`ListBox`]. + pub fn new() -> Self { + Self::with_children(Vec::>::new(), true) + } + + /// Creates a new [`ListBox`]. + /// + /// [`ListBox`]: struct.ListBox.html + pub fn with_children( + children: Vec>, + show_separators: bool, + ) -> Self { + let end = children.len() - 1; + let children: Vec> = children + .into_iter() + .enumerate() + .map(|(index, child)| { + let row_items = if show_separators && index != end { + vec![ + row![child] + .align_items(Alignment::Center) + .into(), + horizontal_rule(1).style(theme::Rule::Custom(separator_style)).into(), + ] + } else { + vec![ + row![child] + .align_items(Alignment::Center) + .into() + ] + }; + column(row_items).into() + }) + .collect(); + Self { + spacing: 0, + padding: Padding::from(Self::DEFAULT_PADDING), + width: Length::Shrink, + height: Length::Shrink, + max_width: u32::MAX, + align_items: Alignment::Start, + style: Default::default(), + children, + placeholder: None, + on_item_selected: None, + } + } + + /// Adds an element to the [`ListBox`]. + pub fn push(mut self, child: impl Into>) -> Self { + self.children.push(child.into()); + self + } +} + +impl<'a, Message: 'a, Renderer: iced_native::Renderer + 'a> std::default::Default + for ListBox<'a, Message, Renderer> +where + Renderer::Theme: StyleSheet + iced_style::rule::StyleSheet, + <::Theme as StyleSheet>::Style: From, + <::Theme as iced_style::rule::StyleSheet>::Style: From +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Renderer> Widget for ListBox<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, + ::Theme: StyleSheet + iced_style::rule::StyleSheet +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node { + let limits = limits + .max_width(self.max_width) + .width(self.width) + .height(self.height); + + if !self.children.is_empty() { + resolve( + Axis::Vertical, + renderer, + &limits, + self.padding, + self.spacing as f32, + self.align_items, + &self.children, + ) + } else if self.placeholder.is_some() { + self.placeholder + .as_ref() + .unwrap() + .as_widget() + .layout(renderer, &limits) + } else { + Node::default() + } + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + let color_scheme = theme.appearance(self.style); + + draw_background(renderer, &color_scheme, layout.bounds()); + + if !self.children.is_empty() { + for ((child, state), layout) in self + .children + .iter() + .zip(&state.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ); + } + } else if let Some(placeholder) = &self.placeholder { + placeholder.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ); + } + } + + fn children(&self) -> Vec { + let widgets = if !self.children.is_empty() { + self.children.iter().map(Tree::new).collect() + } else if let Some(placeholder) = &self.placeholder { + vec![Tree::new(placeholder)] + } else { + vec![Tree::empty()] + }; + widgets + } + + fn diff(&self, tree: &mut Tree) { + if !self.children.is_empty() { + tree.diff_children(&self.children); + } else if let Some(placeholder) = &self.placeholder { + tree.diff_children(&[placeholder]); + } + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation, + ) { + if !self.children.is_empty() { + operation.container(None, &mut |operation| { + self.children + .iter() + .zip(&mut state.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child.as_widget().operate(state, layout, operation); + }) + }); + } else if let Some(placeholder) = &self.placeholder { + placeholder.as_widget().operate(state, layout, operation); + } + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> Status { + if !self.children.is_empty() { + self.children + .iter_mut() + .zip(&mut state.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }) + .fold(Status::Ignored, Status::merge) + } else if self.placeholder.is_some() { + self.placeholder.as_mut().unwrap().as_widget_mut().on_event( + &mut state.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } else { + Status::Ignored + } + } + + fn mouse_interaction( + &self, + state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> Interaction { + if !self.children.is_empty() { + self.children + .iter() + .zip(&state.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, + layout, + cursor_position, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } else if let Some(placeholder) = &self.placeholder { + placeholder.as_widget().mouse_interaction( + &state.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + } else { + Interaction::Idle + } + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + if !self.children.is_empty() { + from_children(&self.children, tree, layout, renderer) + } else if let Some(placeholder) = &self.placeholder { + placeholder + .as_widget() + .overlay(&mut tree.children[0], layout, renderer) + } else { + None + } + } +} + +/// Draws the background of a [`Container`] given its [`Style`] and its `bounds`. +pub fn draw_background( + renderer: &mut Renderer, + appearance: &Appearance, + bounds: Rectangle, +) where + Renderer: iced_native::Renderer, +{ + if appearance.background.is_some() || appearance.border_width > 0.0 { + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: appearance.border_radius, + border_width: appearance.border_width, + border_color: appearance.border_color, + }, + appearance + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } +} + +impl<'a, Message: 'a, Renderer: iced_native::Renderer + 'a> From> + for Element<'a, Message, Renderer> + where ::Theme: StyleSheet + iced_style::rule::StyleSheet +{ + fn from(list_box: ListBox<'a, Message, Renderer>) -> Self { + Self::new(list_box) + } +} + +#[macro_export] +macro_rules! list_box_item { + ($($x:expr),+ $(,)?) => ( + $crate::iced::widget::row![ + column(vec![ + $($x),+ + ]) + ] + ); +} +pub use list_box_item; + +#[macro_export] +macro_rules! list_box_heading { + ($title:expr) => ( + $crate::iced::widget::container( + $crate::iced::widget::row![ + text($title).size(18), + $crate::iced::widget::vertical_space(Length::Fill), + $crate::iced::widget::horizontal_space(Length::Fill) + ] + .height(Length::Fill) + .align_items($crate::iced::alignment::Alignment::Center) + ) + .style($crate::iced::theme::Container::Custom($crate::widget::expander_heading_style)) + .max_height(60) + .padding(10) + ); + ($title:expr, $subtitle:expr) => ( + $crate::iced::widget::container( + $crate::iced::widget::row![ + column( + vec![ + text($title).size(18).into(), + text($subtitle).size(16).into(), + ] + ), + $crate::iced::widget::vertical_space(Length::Fill), + $crate::iced::widget::horizontal_space(Length::Fill) + ] + .height(Length::Fill) + .align_items($crate::iced::alignment::Alignment::Center) + ) + .style($crate::iced::theme::Container::Custom($crate::widget::expander_heading_style)) + .max_height(60) + .padding(10) + ); + ($title:expr, $subtitle:expr, $icon:expr) => ( + $crate::iced::widget::container( + $crate::iced::widget::row![ + container($crate::widget::icon($icon, 20)).padding(10), + column( + vec![ + text($title).size(18).into(), + text($subtitle).size(16).into(), + ] + ), + $crate::iced::widget::vertical_space(Length::Fill), + $crate::iced::widget::horizontal_space(Length::Fill) + ] + .height(Length::Fill) + .align_items($crate::iced::alignment::Alignment::Center) + ) + .style($crate::iced::theme::Container::Custom($crate::widget::expander_heading_style)) + .max_height(60) + .padding(10) + ); +} +pub use list_box_heading; + +#[macro_export] +macro_rules! list_box_row { + ($title:expr) => ( + $crate::iced::widget::container( + $crate::iced::widget::row![ + text($title).size(18), + $crate::iced::widget::vertical_space(Length::Fill), + $crate::iced::widget::horizontal_space(Length::Fill) + ] + .height(Length::Fill) + .align_items($crate::iced::alignment::Alignment::Center) + ) + .max_height(60) + .padding(10) + ); + ($title:expr, $subtitle:expr) => ( + $crate::iced::widget::container( + $crate::iced::widget::row![ + column( + vec![ + text($title).size(18).into(), + text($subtitle).size(16).into(), + ] + ), + $crate::iced::widget::vertical_space(Length::Fill), + $crate::iced::widget::horizontal_space(Length::Fill) + ] + .height(Length::Fill) + .align_items($crate::iced::alignment::Alignment::Center) + ) + .max_height(60) + .padding(10) + ); + ($title:expr, $subtitle:expr, $icon:expr) => ( + $crate::iced::widget::container( + $crate::iced::widget::row![ + container($crate::widget::icon($icon, 20)).padding(10), + column( + vec![ + text($title).size(18).into(), + text($subtitle).size(16).into(), + ] + ), + $crate::iced::widget::vertical_space(Length::Fill), + $crate::iced::widget::horizontal_space(Length::Fill) + ] + .height(Length::Fill) + .align_items($crate::iced::alignment::Alignment::Center) + ) + .max_height(60) + .padding(10) + ); +} +pub use list_box_row; +use crate::widget::separator_style; diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 101ea8e..1841895 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -24,3 +24,6 @@ pub use scrollable::*; mod expander; pub use expander::*; + +pub mod list_box; +pub use list_box::*; \ No newline at end of file diff --git a/src/widget/nav.rs b/src/widget/nav.rs index f1a393e..1903cd6 100644 --- a/src/widget/nav.rs +++ b/src/widget/nav.rs @@ -1,9 +1,4 @@ -use iced::{ - Background, - Color, - Theme, - widget -}; +use iced::{widget, Background, Color, Theme}; #[macro_export] macro_rules! nav_bar { @@ -42,21 +37,16 @@ pub fn nav_bar_style(theme: &Theme) -> widget::container::Appearance { #[macro_export] macro_rules! nav_button { - ($icon: expr, $title:expr, $condensed:expr) => ({ + ($icon: expr, $title:expr, $condensed:expr) => {{ if $condensed { - $crate::iced::widget::Button::new( - $crate::widget::icon($icon, 22) - ) - .padding(8) + $crate::iced::widget::Button::new($crate::widget::icon($icon, 22)).padding(8) } else { $crate::widget::button!( $crate::widget::icon($icon, 22), $crate::iced::widget::Text::new($title), - $crate::iced::widget::horizontal_space( - $crate::iced::Length::Fill - ), + $crate::iced::widget::horizontal_space($crate::iced::Length::Fill), ) } - }); + }}; } pub use nav_button; diff --git a/src/widget/navbar.rs b/src/widget/navbar.rs index d8f04d1..c15c897 100644 --- a/src/widget/navbar.rs +++ b/src/widget/navbar.rs @@ -1,26 +1,15 @@ use iced::{ - Element, - Padding, - Length, - Alignment, - widget::{ - Container, - Column, - scrollable - }, - alignment + alignment, + widget::{scrollable, Column, Container}, + Alignment, Element, Length, Padding, }; use iced_native::{ - Widget, + renderer, row, widget::{ - Tree, - container::{ - layout, - draw_background - } - }, - row, - renderer + container::{draw_background, layout}, + Tree, + }, + Widget, }; use iced_style::container::StyleSheet; @@ -45,27 +34,25 @@ where } impl<'a, Message: 'a, Renderer> NavBar<'a, Message, Renderer> -where +where Renderer: iced_native::Renderer + 'a, Renderer::Theme: StyleSheet, { /// Creates a [`NavBar`] with the given elements. - pub fn with_children( - children: Vec>, - ) -> Self where ::Theme: iced_style::scrollable::StyleSheet { + pub fn with_children(children: Vec>) -> Self + where + ::Theme: iced_style::scrollable::StyleSheet, + { let nav = Self::default(); NavBar { content: Container::new( - scrollable( - row![ - Column::with_children(children) - .spacing(nav.spacing) - .padding(nav.padding) - ] - ) + scrollable(row![Column::with_children(children) + .spacing(nav.spacing) + .padding(nav.padding)]) .scrollbar_width(6) - .scroller_width(6) - ).into(), + .scroller_width(6), + ) + .into(), ..Default::default() } } @@ -74,7 +61,7 @@ where self.condensed = condensed; self } - + pub fn active(mut self, active: bool) -> Self { self.active = active; self @@ -126,22 +113,19 @@ where } /// Sets the style of the [`NavBar`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { + pub fn style(mut self, style: impl Into<::Style>) -> Self { self.style = style.into(); self } } impl<'a, Message: 'a, Renderer> Default for NavBar<'a, Message, Renderer> -where +where Renderer: iced_native::Renderer + 'a, Renderer::Theme: StyleSheet, { fn default() -> Self { - Self { + Self { spacing: 12, padding: Padding::new(12), width: Length::Shrink, @@ -152,27 +136,18 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, style: Default::default(), - condensed: false, - active: true, + condensed: false, + active: true, content: Container::new(row![Column::new()]).into(), } } } -impl<'a, Message, Renderer> Widget - for NavBar<'a, Message, Renderer> +impl<'a, Message, Renderer> Widget for NavBar<'a, Message, Renderer> where Renderer: iced_native::Renderer, Renderer::Theme: StyleSheet, { - fn children(&self) -> Vec { - vec![Tree::new(&self.content)] - } - - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) - } - fn width(&self) -> Length { self.width } @@ -191,11 +166,7 @@ where limits, self.width, self.height, - if self.condensed { - 100 - } else { - self.max_width - }, + if self.condensed { 100 } else { self.max_width }, self.max_height, if self.active { self.padding @@ -208,13 +179,51 @@ where if self.active { self.content.as_widget().layout(renderer, limits) } else { - let content: Element = Container::new(row![Column::new()]).into(); + let content: Element = + Container::new(row![Column::new()]).into(); content.as_widget().layout(renderer, limits) } }, ) } - + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + renderer_style: &iced_native::renderer::Style, + layout: iced_native::Layout<'_>, + cursor_position: iced::Point, + viewport: &iced::Rectangle, + ) { + if self.active { + let style = theme.appearance(self.style); + + draw_background(renderer, &style, layout.bounds()); + + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &renderer::Style { + text_color: style.text_color.unwrap_or(renderer_style.text_color), + }, + layout.children().next().unwrap(), + cursor_position, + viewport, + ); + } + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + fn operate( &self, tree: &mut Tree, @@ -268,37 +277,6 @@ where ) } - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - renderer_style: &iced_native::renderer::Style, - layout: iced_native::Layout<'_>, - cursor_position: iced::Point, - viewport: &iced::Rectangle, - ) { - if self.active { - let style = theme.appearance(self.style); - - draw_background(renderer, &style, layout.bounds()); - - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - &renderer::Style { - text_color: style - .text_color - .unwrap_or(renderer_style.text_color), - }, - layout.children().next().unwrap(), - cursor_position, - viewport, - ); - } - } - fn overlay<'b>( &'b self, tree: &'b mut iced_native::widget::Tree, @@ -311,11 +289,9 @@ where renderer, ) } - } -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> +impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where Message: 'a, Renderer: iced_native::Renderer + 'a, @@ -334,4 +310,4 @@ macro_rules! navbar { ($($x:expr),+ $(,)?) => ( $crate::widget::NavBar::with_children(vec![$($crate::iced::Element::from($x)),+]) ); -} \ No newline at end of file +} diff --git a/src/widget/scrollable.rs b/src/widget/scrollable.rs index 1fa1772..9b7acef 100644 --- a/src/widget/scrollable.rs +++ b/src/widget/scrollable.rs @@ -1,8 +1,8 @@ #[macro_export] macro_rules! scrollable { - ($x:expr) => ( + ($x:expr) => { $crate::iced::widget::scrollable($x) .scrollbar_width(8) .scroller_width(8) - ); -} \ No newline at end of file + }; +} diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 437398d..78e16b0 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -1,7 +1,4 @@ -use iced::{ - Length, - widget, -}; +use iced::{widget, Length}; pub fn toggler<'a, Message, Renderer>( label: impl Into>,