diff --git a/.gitignore b/.gitignore index 96ef6c0b..6a59f558 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +/.idea \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 976bb194..ce06f64b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,27 +3,68 @@ name = "libcosmic" version = "0.1.0" edition = "2021" -[dependencies] -cascade = "1.0.0" -derivative = { version = "2", optional = true } -gtk4 = { git = "https://github.com/gtk-rs/gtk4-rs", features = ["v4_6"] } -adw = { git = "https://gitlab.gnome.org/World/Rust/libadwaita-rs", package = "libadwaita"} -adw-user-colors-lib = { git = "https://github.com/pop-os/user-color-editor" } -gdk4-wayland = { git = "https://github.com/gtk-rs/gtk4-rs", features = ["wayland_crate"], optional = true } -gdk4-x11 = { git = "https://github.com/gtk-rs/gtk4-rs", features = ["xlib"], optional = true } -x11 = { version = "2.19.1", features = ["xlib"], optional = true } -gobject-sys = { git = "https://github.com/gtk-rs/gtk-rs-core", optional = true } -wayland-client = { version = "0.29.4", optional = true } -wayland-protocols = { version = "0.29.4", features = ["client", "unstable_protocols"], optional = true } -once_cell = "1.13.0" -libcosmic-widgets = { path = "widgets", optional = true } -xdg = "2.4.1" +[lib] +name = "cosmic" [features] -default = ["widgets"] -layer-shell = ["derivative", "gdk4-wayland", "wayland-client", "wayland-protocols", "gobject-sys"] -x = ["x11", "gdk4-x11"] -widgets = ["libcosmic-widgets"] +default = ["wayland"] +debug = ["iced/debug"] +wayland = ["iced/wayland"] +wgpu = ["iced/wgpu"] +winit = ["iced/winit", "iced_winit"] +applet = ["cosmic-panel-config", "sctk"] + +[dependencies] +freedesktop-icons = {git = "https://github.com/wash2/freedestkop-icons"} +apply = "0.3.0" +derive_setters = "0.1.5" +lazy_static = "1.4.0" +palette = "0.6.1" +cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", default-features = false, optional = true } +sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", optional = true } + +[dependencies.cosmic-theme] +git = "https://github.com/pop-os/cosmic-theme.git" + +[dependencies.iced] +git = "https://github.com/pop-os/iced.git" +branch = "sctk-cosmic" +# path = "../iced" +default-features = false +features = ["image", "svg"] + +[dependencies.iced_core] +git = "https://github.com/pop-os/iced.git" +branch = "sctk-cosmic" +# path = "../iced/core" + +[dependencies.iced_lazy] +git = "https://github.com/pop-os/iced.git" +branch = "sctk-cosmic" +# path = "../iced/lazy" + +[dependencies.iced_native] +git = "https://github.com/pop-os/iced.git" +branch = "sctk-cosmic" +# path = "../iced/native" + +[dependencies.iced_style] +git = "https://github.com/pop-os/iced.git" +branch = "sctk-cosmic" +# path = "../iced/style" + +[dependencies.iced_winit] +git = "https://github.com/pop-os/iced.git" +branch = "sctk-cosmic" +optional = true +# path = "../iced/winit" + +[dependencies.iced_wgpu] +git = "https://github.com/pop-os/iced.git" +branch = "sctk-cosmic" +# path = "../iced/wgpu" [workspace] -members = ["widgets"] +members = [ + "examples/*", +] diff --git a/README.md b/README.md index 88a2bb4b..5a95cfc2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,35 @@ -# libcosmic -WIP +# LIBCOSMIC + +Building blocks for COSMIC applications. + +## Building +Libcosmic is written in pure Rust, so `cargo` is all you need. + +```shell +cargo build +``` + +## Usage +There's examples in the `examples` directory. + +### Widget library +```shell +cargo run --release --example cosmic +``` + +### Text rendering +```shell +cargo run --release --example text +``` + +## Documentation +The documentation can be found [here](https://pop-os.github.io/docs/). + +## Licence +Libcosmic is licenced under the MPL-2.0 + +## Contact +- [Mattermost](https://chat.pop-os.org/) +- [Discord](https://chat.pop-os.org/) +- [Twitter](https://twitter.com/pop_os_official) +- [Instagram](https://www.instagram.com/pop_os_official/) \ No newline at end of file diff --git a/examples/cosmic-sctk/Cargo.toml b/examples/cosmic-sctk/Cargo.toml new file mode 100644 index 00000000..ec6669e7 --- /dev/null +++ b/examples/cosmic-sctk/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "cosmic_sctk" +version = "0.1.0" +authors = [] +edition = "2021" +publish = false + +[dependencies] +libcosmic = { path = "../..", default-features = false, features = ["debug", "wayland"] } +iced_sctk = { git = "https://github.com/pop-os/iced-sctk" } diff --git a/examples/cosmic-sctk/README.md b/examples/cosmic-sctk/README.md new file mode 100644 index 00000000..c52803b3 --- /dev/null +++ b/examples/cosmic-sctk/README.md @@ -0,0 +1,9 @@ +# COSMIC +An example of the COSMIC design system. + +All the example code is located in the __[`main`](src/main.rs)__ file. + +You can run it with `cargo run`: +``` +cargo run --package cosmic --release +``` diff --git a/examples/cosmic-sctk/src/main.rs b/examples/cosmic-sctk/src/main.rs new file mode 100644 index 00000000..e6a2e4b6 --- /dev/null +++ b/examples/cosmic-sctk/src/main.rs @@ -0,0 +1,13 @@ +use cosmic::{ + iced::{sctk_settings::InitialSurface, Application}, + settings, +}; + +mod window; +pub use window::Window; + +pub fn main() -> cosmic::iced::Result { + let mut settings = settings(); + settings.initial_surface = InitialSurface::XdgWindow(Default::default()); + Window::run(settings) +} diff --git a/examples/cosmic-sctk/src/window.rs b/examples/cosmic-sctk/src/window.rs new file mode 100644 index 00000000..f588bb9d --- /dev/null +++ b/examples/cosmic-sctk/src/window.rs @@ -0,0 +1,332 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use cosmic::{ + iced_native::window, + iced::widget::{ + column, container, horizontal_space, pick_list, progress_bar, radio, row, slider, + }, + iced::{self, Alignment, Application, Command, Length}, + iced_lazy::responsive, + theme::{self, Theme}, + widget::{button, nav_button, nav_bar, nav_bar_page, nav_bar_section, header_bar, settings, scrollable, toggler}, + Element, + ElementExt, +}; +use iced_sctk::application::SurfaceIdWrapper; +use std::{collections::BTreeMap, vec}; +use theme::Button as ButtonTheme; + +#[derive(Default)] +pub struct Window { + title: String, + 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, + InputChanged, +} + +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.theme = Theme::Light; + window.pick_list_selected = Some("Option 1"); + window.title = String::from("COSMIC Design System - Iced"); + (window, Command::none()) + } + + fn title(&self) -> String { + self.title.clone() + } + + 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 => todo!(), + Message::Minimize => todo!(), + Message::Maximize => todo!(), + Message::RowSelected(row) => println!("Selected row {row}"), + Message::InputChanged => {}, + + } + + Command::none() + } + + fn view(&self, _: SurfaceIdWrapper) -> Element { + let mut header = header_bar() + .title("COSMIC Design System - Iced") + .on_close(Message::Close) + .on_drag(Message::Drag) + .start( + nav_button("Settings") + .on_sidebar_toggled(Message::ToggleSidebar) + .sidebar_active(self.sidebar_toggled) + .into() + ); + + if self.show_maximize { + header = header.on_maximize(Message::Maximize); + } + + if self.show_minimize { + header = header.on_minimize(Message::Minimize); + } + + let header = Into::>::into(header).debug(self.debug); + + // 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; + + // cosmic::navbar![ + // nav_text_button("network-wireless", "Network & Wireless", condensed) + // .on_press(Message::Page(0)) + // .style(if self.page == 0 { + // ButtonTheme::Primary + // } else { + // ButtonTheme::Text + // }), + // nav_text_button("preferences-desktop", "Bluetooth", condensed) + // .on_press(Message::Page(1)) + // .style(if self.page == 1 { + // ButtonTheme::Primary + // } else { + // ButtonTheme::Text + // }), + // nav_text_button("system-software-update", "Personalization", condensed) + // .on_press(Message::Page(2)) + // .style(if self.page == 2 { + // ButtonTheme::Primary + // } else { + // ButtonTheme::Text + // }), + // ] + + let sidebar: Element<_> = nav_bar() + .source(BTreeMap::from([ + ( + nav_bar_section() + .title("Network & Wireless") + .icon("network-wireless"), + vec![nav_bar_page("Wi-Fi")], + ), + ( + nav_bar_section().title("Bluetooth").icon("cs-bluetooth"), + vec![nav_bar_page("Devices")], + ), + ( + nav_bar_section() + .title("Personalization") + .icon("applications-system"), + vec![ + nav_bar_page("Desktop Session"), + nav_bar_page("Wallpaper"), + nav_bar_page("Appearance"), + nav_bar_page("Dock & Top Panel"), + nav_bar_page("Workspaces"), + nav_bar_page("Notifications"), + ], + ), + ( + nav_bar_section() + .title("Input Devices") + .icon("input-keyboard"), + vec![nav_bar_page("Keyboard")], + ), + ( + nav_bar_section().title("Displays").icon("cs-display"), + vec![nav_bar_page("Keyboard")], + ), + ( + nav_bar_section().title("Power & Battery").icon("battery"), + vec![nav_bar_page("Status")], + ), + ( + nav_bar_section().title("Sound").icon("sound"), + vec![nav_bar_page("Volume")], + ), + ])) + .active(self.sidebar_toggled) + .condensed(condensed) + .into(); + + let choose_theme = [Theme::Light, Theme::Dark].iter().fold( + row![].spacing(10).align_items(Alignment::Center), + |row, theme| { + row.push(radio( + format!("{:?}", theme), + *theme, + Some(self.theme), + Message::ThemeChanged, + )) + }, + ); + + let content: Element<_> = settings::view_column(vec![ + settings::view_section("Debug") + .add(settings::item("Debug theme", choose_theme)) + .add(settings::item( + "Debug layout", + toggler(String::from("Debug layout"), self.debug, Message::Debug) + )) + .into(), + settings::view_section("Buttons") + .add(settings::item_row(vec![ + button(ButtonTheme::Primary) + .text("Primary") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Secondary) + .text("Secondary") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Positive) + .text("Positive") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Destructive) + .text("Destructive") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Text) + .text("Text") + .on_press(Message::ButtonPressed) + .into() + ])) + .add(settings::item_row(vec![ + button(ButtonTheme::Primary).text("Primary").into(), + button(ButtonTheme::Secondary).text("Secondary").into(), + button(ButtonTheme::Positive).text("Positive").into(), + button(ButtonTheme::Destructive).text("Destructive").into(), + button(ButtonTheme::Text).text("Text").into(), + ])) + .into(), + settings::view_section("Controls") + .add(settings::item("Toggler", toggler(None, self.toggler_value, Message::TogglerToggled))) + .add(settings::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]) + )) + .add(settings::item( + "Slider", + slider(0.0..=100.0, self.slider_value, Message::SliderChanged) + .width(Length::Units(250)) + )) + .add(settings::item( + "Progress", + progress_bar(0.0..=100.0, self.slider_value) + .width(Length::Units(250)) + .height(Length::Units(4)) + )) + .into() + ]) + .into(); + + let mut widgets = Vec::with_capacity(2); + + widgets.push(sidebar.debug(self.debug)); + + widgets.push( + scrollable(row![ + horizontal_space(Length::Fill), + content.debug(self.debug), + 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 + } + + fn close_requested(&self, id: iced_sctk::application::SurfaceIdWrapper) -> Self::Message { + Message::Close + } +} diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml new file mode 100644 index 00000000..53bf3859 --- /dev/null +++ b/examples/cosmic/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "cosmic" +version = "0.1.0" +authors = [] +edition = "2021" +publish = false + +[dependencies] +libcosmic = { path = "../..", default-features = false, features = ["debug", "wgpu", "winit"] } diff --git a/examples/cosmic/README.md b/examples/cosmic/README.md new file mode 100644 index 00000000..c52803b3 --- /dev/null +++ b/examples/cosmic/README.md @@ -0,0 +1,9 @@ +# COSMIC +An example of the COSMIC design system. + +All the example code is located in the __[`main`](src/main.rs)__ file. + +You can run it with `cargo run`: +``` +cargo run --package cosmic --release +``` diff --git a/examples/cosmic/src/main.rs b/examples/cosmic/src/main.rs new file mode 100644 index 00000000..d289e54a --- /dev/null +++ b/examples/cosmic/src/main.rs @@ -0,0 +1,15 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use cosmic::{iced::Application, settings}; + +mod window; +pub use window::*; + +pub fn main() -> cosmic::iced::Result { + let mut settings = settings(); + settings.window.min_size = Some((600, 300)); + // TODO: Window resize handles not functioning yet + settings.window.decorations = false; + Window::run(settings) +} diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs new file mode 100644 index 00000000..c84c0b8c --- /dev/null +++ b/examples/cosmic/src/window.rs @@ -0,0 +1,328 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use cosmic::{ + iced_native::window, + iced::widget::{ + column, container, horizontal_space, pick_list, progress_bar, radio, row, slider, + }, + iced::{self, Alignment, Application, Command, Length}, + iced_lazy::responsive, + iced_winit::window::{drag, toggle_maximize, minimize}, + theme::{self, Theme}, + widget::{button, nav_button, nav_bar, nav_bar_page, nav_bar_section, header_bar, settings, scrollable, toggler}, + Element, + ElementExt, +}; +use std::{collections::BTreeMap, vec}; +use theme::Button as ButtonTheme; + +#[derive(Default)] +pub struct Window { + title: String, + 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, + InputChanged, +} + +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.theme = Theme::Light; + window.pick_list_selected = Some("Option 1"); + window.title = String::from("COSMIC Design System - Iced"); + (window, Command::none()) + } + + fn title(&self) -> String { + self.title.clone() + } + + 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(window::Id::new(0)), + Message::Minimize => return minimize(window::Id::new(0), true), + Message::Maximize => return toggle_maximize(window::Id::new(0)), + Message::RowSelected(row) => println!("Selected row {row}"), + Message::InputChanged => {}, + + } + + Command::none() + } + + fn view(&self) -> Element { + let mut header = header_bar() + .title("COSMIC Design System - Iced") + .on_close(Message::Close) + .on_drag(Message::Drag) + .start( + nav_button("Settings") + .on_sidebar_toggled(Message::ToggleSidebar) + .sidebar_active(self.sidebar_toggled) + .into() + ); + + if self.show_maximize { + header = header.on_maximize(Message::Maximize); + } + + if self.show_minimize { + header = header.on_minimize(Message::Minimize); + } + + let header = Into::>::into(header).debug(self.debug); + + // 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; + + // cosmic::navbar![ + // nav_text_button("network-wireless", "Network & Wireless", condensed) + // .on_press(Message::Page(0)) + // .style(if self.page == 0 { + // ButtonTheme::Primary + // } else { + // ButtonTheme::Text + // }), + // nav_text_button("preferences-desktop", "Bluetooth", condensed) + // .on_press(Message::Page(1)) + // .style(if self.page == 1 { + // ButtonTheme::Primary + // } else { + // ButtonTheme::Text + // }), + // nav_text_button("system-software-update", "Personalization", condensed) + // .on_press(Message::Page(2)) + // .style(if self.page == 2 { + // ButtonTheme::Primary + // } else { + // ButtonTheme::Text + // }), + // ] + + let sidebar: Element<_> = nav_bar() + .source(BTreeMap::from([ + ( + nav_bar_section() + .title("Network & Wireless") + .icon("network-wireless"), + vec![nav_bar_page("Wi-Fi")], + ), + ( + nav_bar_section().title("Bluetooth").icon("cs-bluetooth"), + vec![nav_bar_page("Devices")], + ), + ( + nav_bar_section() + .title("Personalization") + .icon("applications-system"), + vec![ + nav_bar_page("Desktop Session"), + nav_bar_page("Wallpaper"), + nav_bar_page("Appearance"), + nav_bar_page("Dock & Top Panel"), + nav_bar_page("Workspaces"), + nav_bar_page("Notifications"), + ], + ), + ( + nav_bar_section() + .title("Input Devices") + .icon("input-keyboard"), + vec![nav_bar_page("Keyboard")], + ), + ( + nav_bar_section().title("Displays").icon("cs-display"), + vec![nav_bar_page("Keyboard")], + ), + ( + nav_bar_section().title("Power & Battery").icon("battery"), + vec![nav_bar_page("Status")], + ), + ( + nav_bar_section().title("Sound").icon("sound"), + vec![nav_bar_page("Volume")], + ), + ])) + .active(self.sidebar_toggled) + .condensed(condensed) + .into(); + + let choose_theme = [Theme::Light, Theme::Dark].iter().fold( + row![].spacing(10).align_items(Alignment::Center), + |row, theme| { + row.push(radio( + format!("{:?}", theme), + *theme, + Some(self.theme), + Message::ThemeChanged, + )) + }, + ); + + let content: Element<_> = settings::view_column(vec![ + settings::view_section("Debug") + .add(settings::item("Debug theme", choose_theme)) + .add(settings::item( + "Debug layout", + toggler(String::from("Debug layout"), self.debug, Message::Debug) + )) + .into(), + settings::view_section("Buttons") + .add(settings::item_row(vec![ + button(ButtonTheme::Primary) + .text("Primary") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Secondary) + .text("Secondary") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Positive) + .text("Positive") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Destructive) + .text("Destructive") + .on_press(Message::ButtonPressed) + .into(), + button(ButtonTheme::Text) + .text("Text") + .on_press(Message::ButtonPressed) + .into() + ])) + .add(settings::item_row(vec![ + button(ButtonTheme::Primary).text("Primary").into(), + button(ButtonTheme::Secondary).text("Secondary").into(), + button(ButtonTheme::Positive).text("Positive").into(), + button(ButtonTheme::Destructive).text("Destructive").into(), + button(ButtonTheme::Text).text("Text").into(), + ])) + .into(), + settings::view_section("Controls") + .add(settings::item("Toggler", toggler(None, self.toggler_value, Message::TogglerToggled))) + .add(settings::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]) + )) + .add(settings::item( + "Slider", + slider(0.0..=100.0, self.slider_value, Message::SliderChanged) + .width(Length::Units(250)) + )) + .add(settings::item( + "Progress", + progress_bar(0.0..=100.0, self.slider_value) + .width(Length::Units(250)) + .height(Length::Units(4)) + )) + .into() + ]) + .into(); + + let mut widgets = Vec::with_capacity(2); + + widgets.push(sidebar.debug(self.debug)); + + widgets.push( + scrollable(row![ + horizontal_space(Length::Fill), + content.debug(self.debug), + 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/res/Fira/FiraMono-Regular.otf b/res/Fira/FiraMono-Regular.otf new file mode 100644 index 00000000..c30b25b9 Binary files /dev/null and b/res/Fira/FiraMono-Regular.otf differ diff --git a/res/Fira/FiraSans-Light.otf b/res/Fira/FiraSans-Light.otf new file mode 100644 index 00000000..1445a4af Binary files /dev/null and b/res/Fira/FiraSans-Light.otf differ diff --git a/res/Fira/FiraSans-Regular.otf b/res/Fira/FiraSans-Regular.otf new file mode 100644 index 00000000..98ef98c8 Binary files /dev/null and b/res/Fira/FiraSans-Regular.otf differ diff --git a/res/Fira/FiraSans-SemiBold.otf b/res/Fira/FiraSans-SemiBold.otf new file mode 100644 index 00000000..6f7204d8 Binary files /dev/null and b/res/Fira/FiraSans-SemiBold.otf differ diff --git a/res/Fira/SIL Open Font License.txt b/res/Fira/SIL Open Font License.txt new file mode 100644 index 00000000..8ad18250 --- /dev/null +++ b/res/Fira/SIL Open Font License.txt @@ -0,0 +1,48 @@ +Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. +with Reserved Font Name < Fira >, + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/src/applet/mod.rs b/src/applet/mod.rs new file mode 100644 index 00000000..b7d5be45 --- /dev/null +++ b/src/applet/mod.rs @@ -0,0 +1,130 @@ +use cosmic_panel_config::{PanelAnchor, PanelSize}; +use iced::{ + alignment::{Horizontal, Vertical}, + widget::{self, Button}, + Color, Element, Length, Rectangle, +}; +use iced_native::command::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; +use iced_style::container::Appearance; +use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; + +use crate::{ + button, + theme::{self, Container}, + widget::icon, +}; + +pub fn icon_button<'a, M: 'a, Renderer>( + name: &str, + icon_style: ::Style, +) -> Button<'a, M, Renderer> +where + Renderer::Theme: iced_native::svg::StyleSheet + iced_style::button::StyleSheet, + Renderer: iced_native::Renderer + iced_native::svg::Renderer + 'a, +{ + let pixels = std::env::var("COSMIC_PANEL_SIZE") + .ok() + .and_then(|size| match size.parse::() { + Ok(PanelSize::XL) => Some(64), + Ok(PanelSize::L) => Some(36), + Ok(PanelSize::M) => Some(24), + Ok(PanelSize::S) => Some(16), + Ok(PanelSize::XS) => Some(12), + Err(_) => Some(12), + }) + .unwrap_or(16); + button!(icon(name, pixels).style(icon_style)) +} + +pub fn get_popup_settings( + parent: iced_native::window::Id, + id: iced_native::window::Id, + size: (u32, u32), + width_padding: Option, + height_padding: Option, +) -> SctkPopupSettings { + let anchor = std::env::var("COSMIC_PANEL_ANCHOR") + .ok() + .map(|size| match size.parse::() { + Ok(p) => p, + Err(_) => PanelAnchor::Top, + }) + .unwrap_or(PanelAnchor::Top); + let pixels = std::env::var("COSMIC_PANEL_SIZE") + .ok() + .and_then(|size| match size.parse::() { + Ok(PanelSize::XL) => Some(64), + Ok(PanelSize::L) => Some(36), + Ok(PanelSize::M) => Some(24), + Ok(PanelSize::S) => Some(16), + Ok(PanelSize::XS) => Some(12), + Err(_) => Some(12), + }) + .unwrap_or(16); + let (offset, anchor, gravity) = match anchor { + PanelAnchor::Left => ((8, 0), Anchor::Right, Gravity::Right), + PanelAnchor::Right => ((-8, 0), Anchor::Left, Gravity::Left), + PanelAnchor::Top => ((0, 8), Anchor::Bottom, Gravity::Bottom), + PanelAnchor::Bottom => ((0, -8), Anchor::Top, Gravity::Top), + }; + SctkPopupSettings { + parent, + id, + positioner: SctkPositioner { + anchor, + gravity, + offset, + size, + anchor_rect: Rectangle { + x: 0, + y: 0, + width: width_padding.unwrap_or(16) * 2 + pixels as i32, + height: height_padding.unwrap_or(8) * 2 + pixels as i32, + }, + reactive: true, + constraint_adjustment: 15, // slide_y, slide_x, flip_x, flip_y + ..Default::default() + }, + parent_size: None, + grab: true, + } +} + +// TODO popup container which tracks the size of itself and requests the popup to resize to match +pub fn popup_container<'a, Message, Renderer>( + content: impl Into>, +) -> crate::widget::widget::Container<'a, Message, Renderer> +where + Renderer: iced_native::Renderer + 'a, + Message: 'a, + <::Theme as iced_style::container::StyleSheet>::Style: + From, + Renderer::Theme: widget::container::StyleSheet, +{ + let anchor = std::env::var("COSMIC_PANEL_ANCHOR") + .ok() + .map(|size| match size.parse::() { + Ok(p) => p, + Err(_) => PanelAnchor::Top, + }) + .unwrap_or(PanelAnchor::Top); + let (valign, halign) = match anchor { + PanelAnchor::Left => (Vertical::Center, Horizontal::Left), + PanelAnchor::Right => (Vertical::Center, Horizontal::Right), + PanelAnchor::Top => (Vertical::Top, Horizontal::Center), + PanelAnchor::Bottom => (Vertical::Bottom, Horizontal::Center), + }; + crate::widget::widget::container(crate::widget::widget::container(content).style( + Container::Custom(|theme| Appearance { + text_color: Some(theme.cosmic().on_bg_color().into()), + background: Some(theme.extended_palette().background.base.color.into()), + border_radius: 12.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }), + )) + .width(Length::Fill) + .height(Length::Fill) + .align_x(halign) + .align_y(valign) +} diff --git a/src/deref_cell.rs b/src/deref_cell.rs deleted file mode 100644 index d29cf10e..00000000 --- a/src/deref_cell.rs +++ /dev/null @@ -1,33 +0,0 @@ -#![allow(dead_code)] - -use once_cell::unsync::OnceCell; - -/// Wrapper around `OnceCell` implementing `Deref`, and thus also panicking -/// when not set (or set twice). -/// -/// To be used in place of `gtk::TemplateChild`, but without xml. -pub struct DerefCell(OnceCell); - -impl DerefCell { - #[track_caller] - pub fn set(&self, value: T) { - if self.0.set(value).is_err() { - panic!("Initialized twice"); - } - } -} - -impl Default for DerefCell { - fn default() -> Self { - Self(OnceCell::default()) - } -} - -impl std::ops::Deref for DerefCell { - type Target = T; - - #[track_caller] - fn deref(&self) -> &T { - self.0.get().unwrap() - } -} diff --git a/src/ext.rs b/src/ext.rs new file mode 100644 index 00000000..1897d574 --- /dev/null +++ b/src/ext.rs @@ -0,0 +1,19 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use iced::Color; + +pub trait ElementExt { + #[must_use] + fn debug(self, debug: bool) -> Self; +} + +impl<'a, Message: 'static> ElementExt for crate::Element<'a, Message> { + fn debug(self, debug: bool) -> Self { + if debug { + self.explain(Color::WHITE) + } else { + self + } + } +} \ No newline at end of file diff --git a/src/font.rs b/src/font.rs new file mode 100644 index 00000000..e3f96983 --- /dev/null +++ b/src/font.rs @@ -0,0 +1,19 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +pub use iced::Font; + +pub const FONT: Font = Font::External { + name: "Fira Sans Regular", + bytes: include_bytes!("../res/Fira/FiraSans-Regular.otf"), +}; + +pub const FONT_LIGHT: Font = Font::External { + name: "Fira Sans Light", + bytes: include_bytes!("../res/Fira/FiraSans-Light.otf"), +}; + +pub const FONT_SEMIBOLD: Font = Font::External { + name: "Fira Sans SemiBold", + bytes: include_bytes!("../res/Fira/FiraSans-SemiBold.otf"), +}; diff --git a/src/lib.rs b/src/lib.rs index 9a7dc041..402ac775 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,101 +1,32 @@ -mod deref_cell; -#[cfg(feature = "layer-shell")] -pub mod wayland; -#[cfg(feature = "layer-shell")] -mod wayland_custom_surface; -#[cfg(feature = "x")] -pub mod x; +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 -#[cfg(feature = "widgets")] -pub use libcosmic_widgets as widgets; +pub use iced; +pub use iced_lazy; +pub use iced_native; +pub use iced_style; +#[cfg(feature = "winit")] +pub use iced_winit; -use adw::StyleManager; -use gtk4::{ - gdk, - gio::{self, FileMonitor, FileMonitorEvent, FileMonitorFlags}, - glib, - prelude::*, -}; +#[cfg(feature = "applet")] +pub mod applet; +pub mod font; +pub mod theme; +pub mod widget; -pub fn init() -> (Option, Option) { - let _ = gtk4::init(); - adw::init(); +mod ext; +pub use ext::ElementExt; - let gtk_user_provider = gtk4::CssProvider::new(); - if let Some(display) = gdk::Display::default() { - gtk4::StyleContext::add_provider_for_display( - &display, - >k_user_provider, - gtk4::STYLE_PROVIDER_PRIORITY_USER, - ); - } +pub use theme::Theme; +pub type Renderer = iced::Renderer; +pub type Element<'a, Message> = iced::Element<'a, Message, Renderer>; - let cosmic_user_provider = gtk4::CssProvider::new(); - if let Some(display) = gdk::Display::default() { - gtk4::StyleContext::add_provider_for_display( - &display, - &cosmic_user_provider, - gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, - ); - } - - let path = xdg::BaseDirectories::with_prefix("gtk-4.0") - .ok() - .and_then(|xdg_dirs| xdg_dirs.find_config_file("gtk.css")) - .unwrap_or_else(|| "~/.config/gtk-4.0/gtk.css".into()); - let gtk_file = gio::File::for_path(path); - let gtk_css_monitor = gtk_file.monitor(FileMonitorFlags::all(), None::<&gio::Cancellable>).ok().map(|monitor| { - monitor.connect_changed(glib::clone!(@strong gtk_user_provider => move |_monitor, file, _other_file, event| { - match event { - FileMonitorEvent::Deleted | FileMonitorEvent::MovedOut | FileMonitorEvent::Renamed => { - if adw::is_initialized() { - let manager = StyleManager::default(); - let css = if manager.is_dark() { - adw_user_colors_lib::colors::ColorOverrides::dark_default().as_gtk_css() - } else { - adw_user_colors_lib::colors::ColorOverrides::light_default().as_gtk_css() - }; - gtk_user_provider - .load_from_data(css.as_bytes()); - } - }, - FileMonitorEvent::ChangesDoneHint | FileMonitorEvent::Created | FileMonitorEvent::MovedIn => { - gtk_user_provider.load_from_file(file); - }, - _ => {} // ignored - } - })); - 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()); - - let cosmic_file = gio::File::for_path(path); - cosmic_user_provider.load_from_file(&cosmic_file); - let cosmic_css_monitor = cosmic_file.monitor(FileMonitorFlags::all(), None::<&gio::Cancellable>).ok().map(|monitor| { - monitor.connect_changed(glib::clone!(@strong cosmic_user_provider => move |_monitor, file, _other_file, event| { - match event { - FileMonitorEvent::Deleted | FileMonitorEvent::MovedOut | FileMonitorEvent::Renamed => { - if adw::is_initialized() { - let manager = StyleManager::default(); - let css = if manager.is_dark() { - adw_user_colors_lib::colors::ColorOverrides::dark_default().as_gtk_css() - } else { - adw_user_colors_lib::colors::ColorOverrides::light_default().as_gtk_css() - }; - cosmic_user_provider - .load_from_data(css.as_bytes()); - } - }, - FileMonitorEvent::ChangesDoneHint | FileMonitorEvent::Created | FileMonitorEvent::MovedIn => { - cosmic_user_provider.load_from_file(file); - }, - _ => {} // ignored - } - })); - monitor - }); - (gtk_css_monitor, cosmic_css_monitor) +pub fn settings() -> iced::Settings { + let mut settings = iced::Settings::default(); + settings.default_font = match font::FONT { + iced::Font::Default => None, + iced::Font::External { bytes, .. } => Some(bytes), + }; + settings.default_text_size = 18; + settings } diff --git a/src/theme/cosmic.rs b/src/theme/cosmic.rs new file mode 100644 index 00000000..a016b62e --- /dev/null +++ b/src/theme/cosmic.rs @@ -0,0 +1,2 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 \ No newline at end of file diff --git a/src/theme/expander.rs b/src/theme/expander.rs new file mode 100644 index 00000000..b2b736e5 --- /dev/null +++ b/src/theme/expander.rs @@ -0,0 +1,59 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use iced_core::{Background, Color}; + +/// The appearance of a [`Expander`](crate::native::expander::Expander). +#[derive(Clone, Copy, Debug)] +pub struct Appearance { + /// The background of the [`Expander`](crate::native::expander::Expander). + pub background: Background, + + /// The border radius of the [`Expander`](crate::native::expander::Expander). + pub border_radius: f32, + + /// The border width of the [`Expander`](crate::native::expander::Expander). + pub border_width: f32, + + /// The border color of the [`Expander`](crate::native::expander::Expander). + pub border_color: Color, + + /// The background of the head of the [`Expander`](crate::native::expander::Expander). + pub head_background: Background, + + /// The text color of the head of the [`Expander`](crate::native::expander::Expander). + pub head_text_color: Color, + + /// The background of the body of the [`Expander`](crate::native::expander::Expander). + pub body_background: Background, + + /// The text color of the body of the [`Expander`](crate::native::expander::Expander). + pub body_text_color: Color, + + /// The color of the close icon of the [`Expander`](crate::native::expander::Expander). + pub toggle_color: Color, +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + background: Color::WHITE.into(), + border_radius: 10.0, //32.0, + border_width: 1.0, + border_color: [0.87, 0.87, 0.87].into(), //Color::BLACK.into(), + head_background: Background::Color([0.87, 0.87, 0.87].into()), + head_text_color: Color::BLACK, + body_background: Color::TRANSPARENT.into(), + body_text_color: Color::BLACK, + toggle_color: Color::BLACK, + } + } +} + +/// A set of rules that dictate the [`Appearance`] of a container. +pub trait StyleSheet { + type Style: Default + Copy; + + /// Produces the [`Appearance`] of a container. + fn appearance(&self, style: Self::Style) -> Appearance; +} diff --git a/src/theme/mod.rs b/src/theme/mod.rs new file mode 100644 index 00000000..9c880375 --- /dev/null +++ b/src/theme/mod.rs @@ -0,0 +1,821 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +pub mod expander; +pub mod palette; + +use std::hash::Hash; +use std::hash::Hasher; + +pub use self::palette::Palette; + +use cosmic_theme::Component; +use iced_style::application; +use iced_style::button; +use iced_style::checkbox; +use iced_style::container; +use iced_style::menu; +use iced_style::pane_grid; +use iced_style::pick_list; +use iced_style::progress_bar; +use iced_style::radio; +use iced_style::rule; +use iced_style::scrollable; +use iced_style::slider; +use iced_style::svg; +use iced_style::text; +use iced_style::text_input; +use iced_style::toggler; + +use iced_core::{Background, Color}; + +type CosmicColor = ::palette::rgb::Srgba; +type CosmicComponent = cosmic_theme::Component; +type CosmicTheme = cosmic_theme::Theme; +type CosmicThemeCss = cosmic_theme::Theme; + +lazy_static::lazy_static! { + pub static ref COSMIC_DARK: CosmicTheme = CosmicThemeCss::dark_default().into_srgba(); + pub static ref COSMIC_LIGHT: CosmicTheme = CosmicThemeCss::light_default().into_srgba(); + pub static ref TRANSPARENT_COMPONENT: Component = Component { + base: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + hover: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + pressed: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + selected: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + selected_text: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + focus: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + divider: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + on: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + on_disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + }; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Theme { + Light, + Dark, +} + +impl Theme { + #[must_use] + pub fn cosmic(self) -> &'static CosmicTheme { + match self { + Self::Dark => &COSMIC_DARK, + Self::Light => &COSMIC_LIGHT, + } + } + + #[must_use] + pub fn palette(self) -> Palette { + match self { + Self::Dark => Palette::DARK, + Self::Light => Palette::LIGHT, + } + } + + #[must_use] + pub fn extended_palette(&self) -> &self::palette::Extended { + match self { + Self::Dark => &self::palette::EXTENDED_DARK, + Self::Light => &self::palette::EXTENDED_LIGHT, + } + } +} + +impl Default for Theme { + fn default() -> Self { + Self::Dark + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Application { + Default, + Custom(fn(Theme) -> application::Appearance), +} + +impl Default for Application { + fn default() -> Self { + Self::Default + } +} + +impl application::StyleSheet for Theme { + type Style = Application; + + fn appearance(&self, style: &Self::Style) -> application::Appearance { + let cosmic = self.cosmic(); + + match style { + Application::Default => application::Appearance { + background_color: cosmic.bg_color().into(), + text_color: cosmic.on_bg_color().into(), + }, + Application::Custom(f) => f(*self), + } + } +} + +/* + * TODO: Button + */ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Button { + Deactivated, + Destructive, + Positive, + Primary, + Secondary, + Text, + Transparent, +} + +impl Default for Button { + fn default() -> Self { + Self::Primary + } +} + +impl Button { + fn cosmic(&self, theme: &Theme) -> &'static CosmicComponent { + let cosmic = theme.cosmic(); + match self { + Button::Primary => &cosmic.accent, + Button::Secondary => &cosmic.primary.component, + Button::Positive => &cosmic.success, + Button::Destructive => &cosmic.destructive, + Button::Text => &cosmic.secondary.component, + Button::Transparent => &TRANSPARENT_COMPONENT, + Button::Deactivated => &cosmic.secondary.component, + } + } +} + +impl button::StyleSheet for Theme { + type Style = Button; + + fn active(&self, style: &Self::Style) -> button::Appearance { + let cosmic = style.cosmic(self); + + button::Appearance { + border_radius: 24.0, + background: match style { + Button::Text => None, + _ => Some(Background::Color(cosmic.base.into())), + }, + text_color: cosmic.on.into(), + ..button::Appearance::default() + } + } + + fn hovered(&self, style: &Self::Style) -> button::Appearance { + let active = self.active(&style); + let cosmic = style.cosmic(self); + + button::Appearance { + background: Some(Background::Color(cosmic.hover.into())), + ..active + } + } +} + +/* + * TODO: Checkbox + */ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Checkbox { + Primary, + Secondary, + Success, + Danger, +} + +impl Default for Checkbox { + fn default() -> Self { + Self::Primary + } +} + +impl checkbox::StyleSheet for Theme { + type Style = Checkbox; + + fn active( + &self, + style: &Self::Style, + is_checked: bool, + ) -> checkbox::Appearance { + let palette = self.extended_palette(); + + match style { + Checkbox::Primary => checkbox_appearance( + palette.primary.strong.text, + palette.background.base, + palette.primary.strong, + is_checked, + ), + Checkbox::Secondary => checkbox_appearance( + palette.background.base.text, + palette.background.base, + palette.background.base, + is_checked, + ), + Checkbox::Success => checkbox_appearance( + palette.success.base.text, + palette.background.base, + palette.success.base, + is_checked, + ), + Checkbox::Danger => checkbox_appearance( + palette.danger.base.text, + palette.background.base, + palette.danger.base, + is_checked, + ), + } + } + + fn hovered( + &self, + style: &Self::Style, + is_checked: bool, + ) -> checkbox::Appearance { + let palette = self.extended_palette(); + + match style { + Checkbox::Primary => checkbox_appearance( + palette.primary.strong.text, + palette.background.weak, + palette.primary.base, + is_checked, + ), + Checkbox::Secondary => checkbox_appearance( + palette.background.base.text, + palette.background.weak, + palette.background.base, + is_checked, + ), + Checkbox::Success => checkbox_appearance( + palette.success.base.text, + palette.background.weak, + palette.success.base, + is_checked, + ), + Checkbox::Danger => checkbox_appearance( + palette.danger.base.text, + palette.background.weak, + palette.danger.base, + is_checked, + ), + } + } +} + +fn checkbox_appearance( + checkmark_color: Color, + base: palette::Pair, + accent: palette::Pair, + is_checked: bool, +) -> checkbox::Appearance { + checkbox::Appearance { + background: Background::Color(if is_checked { accent.color } else { base.color }), + checkmark_color, + border_radius: 4.0, + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: accent.color, + text_color: None, + } +} + +#[derive(Clone, Copy)] +pub enum Expander { + Default, + Custom(fn(&Theme) -> expander::Appearance), +} + +impl Default for Expander { + fn default() -> Self { + Self::Default + } +} + +impl From expander::Appearance> for Expander { + fn from(f: fn(&Theme) -> expander::Appearance) -> Self { + Self::Custom(f) + } +} + +impl expander::StyleSheet for Theme { + type Style = Expander; + + fn appearance(&self, style: Self::Style) -> expander::Appearance { + match style { + Expander::Default => expander::Appearance::default(), + Expander::Custom(f) => f(self), + } + } +} + +/* + * TODO: Container + */ +#[derive(Clone, Copy)] +pub enum Container { + Transparent, + Box, + Custom(fn(&Theme) -> container::Appearance), +} + +impl Default for Container { + fn default() -> Self { + Self::Transparent + } +} + +impl From container::Appearance> for Container { + fn from(f: fn(&Theme) -> container::Appearance) -> Self { + Self::default() + } +} + +impl container::StyleSheet for Theme { + type Style = Container; + + fn appearance(&self, style: &Self::Style) -> container::Appearance { + match style { + Container::Transparent => container::Appearance::default(), + Container::Box => { + let palette = self.extended_palette(); + + container::Appearance { + text_color: None, + background: palette.background.weak.color.into(), + border_radius: 2.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + Container::Custom(f) => f(self), + } + } +} + +/* + * Slider + */ +impl slider::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style) -> slider::Appearance { + let cosmic = self.cosmic(); + + //TODO: no way to set rail thickness + slider::Appearance { + rail_colors: ( + cosmic.accent.base.into(), + //TODO: no way to set color before/after slider + Color::TRANSPARENT, + ), + handle: slider::Handle { + shape: slider::HandleShape::Circle { radius: 10.0 }, + color: cosmic.accent.base.into(), + border_color: Color::TRANSPARENT, + border_width: 0.0, + }, + } + } + + fn hovered(&self, style: &Self::Style) -> slider::Appearance { + let mut style = self.active(&style); + style.handle.shape = slider::HandleShape::Circle { + radius: 16.0 + }; + style.handle.border_width = 6.0; + style.handle.border_color = match self { + Theme::Dark => Color::from_rgba8(0xFF, 0xFF, 0xFF, 0.1), + Theme::Light => Color::from_rgba8(0, 0, 0, 0.1), + }; + style + } + + fn dragging(&self, style: &Self::Style) -> slider::Appearance { + let mut style = self.hovered(&style); + style.handle.border_color = match self { + Theme::Dark => Color::from_rgba8(0xFF, 0xFF, 0xFF, 0.2), + Theme::Light => Color::from_rgba8(0, 0, 0, 0.2), + }; + style + } +} + +/* + * TODO: Menu + */ +impl menu::StyleSheet for Theme { + type Style = (); + + fn appearance(&self, _style: &Self::Style) -> menu::Appearance { + let cosmic = self.cosmic(); + + menu::Appearance { + text_color: cosmic.primary.component.on.into(), + background: Background::Color(cosmic.background.base.into()), + border_width: 0.0, + border_radius: 16.0, + border_color: Color::TRANSPARENT, + selected_text_color: cosmic.primary.component.on.into(), + selected_background: Background::Color(cosmic.primary.component.hover.into()), + } + } +} + +/* + * TODO: Pick List + */ +impl pick_list::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &()) -> pick_list::Appearance { + let cosmic = &self.cosmic().primary.component; + + pick_list::Appearance { + text_color: cosmic.on.into(), + background: Color::TRANSPARENT.into(), + placeholder_color: cosmic.on.into(), + border_radius: 24.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_size: 0.7, + } + } + + fn hovered(&self, style: &()) -> pick_list::Appearance { + let cosmic = &self.cosmic().primary.component; + + pick_list::Appearance { + background: Background::Color(cosmic.hover.into()), + ..self.active(&style) + } + } +} + +/* + * TODO: Radio + */ +impl radio::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style, is_selected: bool) -> radio::Appearance { + let palette = self.extended_palette(); + + radio::Appearance { + background: Color::TRANSPARENT.into(), + dot_color: palette.primary.strong.color, + border_width: 1.0, + border_color: palette.primary.strong.color, + text_color: None, + } + } + + fn hovered(&self, style: &Self::Style, is_selected: bool) -> radio::Appearance { + let active = self.active(&style, is_selected); + let palette = self.extended_palette(); + + radio::Appearance { + dot_color: palette.primary.strong.color, + background: palette.primary.weak.color.into(), + ..active + } + } +} + +/* + * Toggler + */ +impl toggler::StyleSheet for Theme { + type Style = (); + + fn active( + &self, + _style: &Self::Style, + is_active: bool, + ) -> toggler::Appearance { + let palette = self.palette(); + + toggler::Appearance { + background: if is_active { + palette.primary + } else { + //TODO: Grab neutral from palette + match self { + Theme::Dark => Color::from_rgb8(0x78, 0x78, 0x78), + Theme::Light => Color::from_rgb8(0x93, 0x93, 0x93), + } + }, + background_border: None, + //TODO: Grab neutral from palette + foreground: match self { + Theme::Dark => Color::from_rgb8(0x27, 0x27, 0x27), + Theme::Light => Color::from_rgb8(0xe4, 0xe4, 0xe4), + }, + foreground_border: None, + } + } + + fn hovered( + &self, + style: &Self::Style, + is_active: bool, + ) -> toggler::Appearance { + //TODO: grab colors from palette + match self { + Theme::Dark => toggler::Appearance { + background: if is_active { + Color::from_rgb8(0x9f, 0xed, 0xed) + } else { + Color::from_rgb8(0xb6, 0xb6, 0xb6) + }, + ..self.active(&style, is_active) + }, + Theme::Light => toggler::Appearance { + background: if is_active { + Color::from_rgb8(0x00, 0x42, 0x62) + } else { + Color::from_rgb8(0x54, 0x54, 0x54) + }, + ..self.active(&style, is_active) + } + } + } +} + +/* + * TODO: Pane Grid + */ +impl pane_grid::StyleSheet for Theme { + type Style = (); + + fn picked_split(&self, _style: &Self::Style) -> Option { + let palette = self.extended_palette(); + + Some(pane_grid::Line { + color: palette.primary.strong.color, + width: 2.0, + }) + } + + fn hovered_split(&self, _style: &Self::Style) -> Option { + let palette = self.extended_palette(); + + Some(pane_grid::Line { + color: palette.primary.base.color, + width: 2.0, + }) + } +} + +/* + * TODO: Progress Bar + */ +#[derive(Clone, Copy)] +pub enum ProgressBar { + Primary, + Success, + Danger, + Custom(fn(&Theme) -> progress_bar::Appearance), +} + +impl Default for ProgressBar { + fn default() -> Self { + Self::Primary + } +} + +impl progress_bar::StyleSheet for Theme { + type Style = ProgressBar; + + fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance { + let palette = self.extended_palette(); + + let from_palette = |bar: Color| progress_bar::Appearance { + background: palette.background.strong.color.into(), + bar: bar.into(), + border_radius: 2.0, + }; + + match style { + ProgressBar::Primary => from_palette(palette.primary.base.color), + ProgressBar::Success => from_palette(palette.success.base.color), + ProgressBar::Danger => from_palette(palette.danger.base.color), + ProgressBar::Custom(f) => f(self), + } + } +} + +/* + * TODO: Rule + */ +#[derive(Clone, Copy)] +pub enum Rule { + Default, + Custom(fn(&Theme) -> rule::Appearance), +} + +impl Default for Rule { + fn default() -> Self { + Self::Default + } +} + +impl rule::StyleSheet for Theme { + type Style = Rule; + + fn appearance(&self, style: &Self::Style) -> rule::Appearance { + let palette = self.extended_palette(); + + match style { + Rule::Default => rule::Appearance { + color: palette.background.strong.color, + width: 1, + radius: 0.0, + fill_mode: rule::FillMode::Full, + }, + Rule::Custom(f) => f(self), + } + } +} + +/* + * TODO: Scrollable + */ +impl scrollable::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style) -> scrollable::Scrollbar { + let palette = self.extended_palette(); + + scrollable::Scrollbar { + background: palette.background.weak.color.into(), + border_radius: 4.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: palette.background.strong.color, + border_radius: 4.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self, _style: &Self::Style) -> scrollable::Scrollbar { + let palette = self.extended_palette(); + + scrollable::Scrollbar { + background: palette.background.weak.color.into(), + border_radius: 4.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: palette.primary.strong.color, + border_radius: 4.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } +} + +#[derive(Default, Clone, Copy)] +pub enum Svg { + /// Apply a custom appearance filter + Custom(fn(&Theme) -> svg::Appearance), + /// No filtering is applied + #[default] + Default, + /// Icon fill color will match text color + Symbolic, + /// Icon fill color will match accent color + SymbolicActive, +} + +impl Hash for Svg { + fn hash(&self, state: &mut H) { + let id = match self { + Svg::Custom(_) => 0, + Svg::Default => 1, + Svg::Symbolic => 2, + Svg::SymbolicActive => 3 + }; + + id.hash(state); + } +} + +impl svg::StyleSheet for Theme { + type Style = Svg; + + fn appearance(&self, style: Self::Style) -> svg::Appearance { + match style { + Svg::Default => svg::Appearance::default(), + Svg::Custom(appearance) => appearance(self), + Svg::Symbolic => svg::Appearance { + fill: Some(self.extended_palette().background.base.text), + }, + Svg::SymbolicActive => svg::Appearance { + fill: Some(self.cosmic().accent.base.into()), + }, + } + } +} + +/* + * TODO: Text + */ +#[derive(Clone, Copy, Default)] +pub enum Text { + Accent, + #[default] + Default, + Color(Color), + Custom(fn(&Theme) -> text::Appearance), +} + +impl From for Text { + fn from(color: Color) -> Self { + Text::Color(color) + } +} + +impl text::StyleSheet for Theme { + type Style = Text; + + fn appearance(&self, style: Self::Style) -> text::Appearance { + match style { + Text::Accent => text::Appearance { + color: Some(self.cosmic().accent.base.into()), + }, + Text::Default => text::Appearance::default(), + Text::Color(c) => text::Appearance { color: Some(c) }, + Text::Custom(f) => f(self), + } + } +} + +/* + * TODO: Text Input + */ +impl text_input::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style) -> text_input::Appearance { + let palette = self.extended_palette(); + + text_input::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0, + border_width: 1.0, + border_color: palette.background.strong.color, + } + } + + fn hovered(&self, _style: &Self::Style) -> text_input::Appearance { + let palette = self.extended_palette(); + + text_input::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0, + border_width: 1.0, + border_color: palette.background.base.text, + } + } + + fn focused(&self, _style: &Self::Style) -> text_input::Appearance { + let palette = self.extended_palette(); + + text_input::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0, + border_width: 1.0, + border_color: palette.primary.strong.color, + } + } + + fn placeholder_color(&self, _style: &Self::Style) -> Color { + let palette = self.extended_palette(); + + palette.background.strong.color + } + + fn value_color(&self, _style: &Self::Style) -> Color { + let palette = self.extended_palette(); + + palette.background.base.text + } + + fn selection_color(&self, _style: &Self::Style) -> Color { + let palette = self.extended_palette(); + + palette.primary.weak.color + } +} diff --git a/src/theme/palette.rs b/src/theme/palette.rs new file mode 100644 index 00000000..bd330836 --- /dev/null +++ b/src/theme/palette.rs @@ -0,0 +1,286 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +//TODO: GET CORRECT PALETTE FROM COSMIC-THEME +use iced_core::Color; + +use lazy_static::lazy_static; +use palette::{FromColor, Hsl, Mix, RelativeContrast, Srgb}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Palette { + pub background: Color, + pub text: Color, + pub primary: Color, + pub success: Color, + pub danger: Color, +} + +impl Palette { + pub const LIGHT: Self = Self { + background: Color::from_rgb( + 0xee as f32 / 255.0, + 0xee as f32 / 255.0, + 0xee as f32 / 255.0, + ), + text: Color::from_rgb( + 0x00 as f32 / 255.0, + 0x00 as f32 / 255.0, + 0x00 as f32 / 255.0, + ), + primary: Color::from_rgb( + 0x00 as f32 / 255.0, + 0x49 as f32 / 255.0, + 0x6d as f32 / 255.0, + ), + success: Color::from_rgb( + 0x3b as f32 / 255.0, + 0x6e as f32 / 255.0, + 0x43 as f32 / 255.0, + ), + danger: Color::from_rgb( + 0xa0 as f32 / 255.0, + 0x25 as f32 / 255.0, + 0x2b as f32 / 255.0, + ), + }; + + pub const DARK: Self = Self { + background: Color::from_rgb( + 0x1e as f32 / 255.0, + 0x1e as f32 / 255.0, + 0x1e as f32 / 255.0, + ), + text: Color::from_rgb( + 0xe4 as f32 / 255.0, + 0xe4 as f32 / 255.0, + 0xe4 as f32 / 255.0, + ), + primary: Color::from_rgb( + 0x94 as f32 / 255.0, + 0xeb as f32 / 255.0, + 0xeb as f32 / 255.0, + ), + success: Color::from_rgb( + 0xac as f32 / 255.0, + 0xf7 as f32 / 255.0, + 0xd2 as f32 / 255.0, + ), + danger: Color::from_rgb( + 0xff as f32 / 255.0, + 0xb5 as f32 / 255.0, + 0xb5 as f32 / 255.0, + ), + }; +} + +pub struct Extended { + pub background: Background, + pub primary: Primary, + pub secondary: Secondary, + pub success: Success, + pub danger: Danger, +} + +lazy_static! { + pub static ref EXTENDED_LIGHT: Extended = Extended::generate(Palette::LIGHT); + pub static ref EXTENDED_DARK: Extended = Extended::generate(Palette::DARK); +} + +impl Extended { + #[must_use] + pub fn generate(palette: Palette) -> Self { + Self { + background: Background::new(palette.background, palette.text), + primary: Primary::generate(palette.primary, palette.background, palette.text), + secondary: Secondary::generate(palette.background, palette.text), + success: Success::generate(palette.success, palette.background, palette.text), + danger: Danger::generate(palette.danger, palette.background, palette.text), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Pair { + pub color: Color, + pub text: Color, +} + +impl Pair { + pub fn new(color: Color, text: Color) -> Self { + Self { + color, + text: readable(color, text), + } + } +} + +pub struct Background { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Background { + #[must_use] + pub fn new(base: Color, text: Color) -> Self { + let weak = mix(base, text, 0.15); + let strong = mix(base, text, 0.40); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +pub struct Primary { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Primary { + #[must_use] + pub fn generate(base: Color, background: Color, text: Color) -> Self { + let weak = mix(base, background, 0.4); + let strong = deviate(base, 0.1); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +pub struct Secondary { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Secondary { + #[must_use] + pub fn generate(base: Color, text: Color) -> Self { + let base = mix(base, text, 0.2); + let weak = mix(base, text, 0.1); + let strong = mix(base, text, 0.3); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +pub struct Success { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Success { + #[must_use] + pub fn generate(base: Color, background: Color, text: Color) -> Self { + let weak = mix(base, background, 0.4); + let strong = deviate(base, 0.1); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +pub struct Danger { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Danger { + #[must_use] + pub fn generate(base: Color, background: Color, text: Color) -> Self { + let weak = mix(base, background, 0.4); + let strong = deviate(base, 0.1); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +fn darken(color: Color, amount: f32) -> Color { + let mut hsl = to_hsl(color); + + hsl.lightness = if hsl.lightness - amount < 0.0 { + 0.0 + } else { + hsl.lightness - amount + }; + + from_hsl(hsl) +} + +fn lighten(color: Color, amount: f32) -> Color { + let mut hsl = to_hsl(color); + + hsl.lightness = if hsl.lightness + amount > 1.0 { + 1.0 + } else { + hsl.lightness + amount + }; + + from_hsl(hsl) +} + +fn deviate(color: Color, amount: f32) -> Color { + if is_dark(color) { + lighten(color, amount) + } else { + darken(color, amount) + } +} + +fn mix(a: Color, b: Color, factor: f32) -> Color { + let a_lin = Srgb::from(a).into_linear(); + let b_lin = Srgb::from(b).into_linear(); + + let mixed = a_lin.mix(&b_lin, factor); + Srgb::from_linear(mixed).into() +} + +fn readable(background: Color, text: Color) -> Color { + if is_readable(background, text) { + text + } else if is_dark(background) { + Color::WHITE + } else { + Color::BLACK + } +} + +fn is_dark(color: Color) -> bool { + to_hsl(color).lightness < 0.6 +} + +fn is_readable(a: Color, b: Color) -> bool { + let a_srgb = Srgb::from(a); + let b_srgb = Srgb::from(b); + + a_srgb.has_enhanced_contrast_text(&b_srgb) +} + +fn to_hsl(color: Color) -> Hsl { + Hsl::from_color(Srgb::from(color)) +} + +fn from_hsl(hsl: Hsl) -> Color { + Srgb::from_color(hsl).into() +} diff --git a/src/wayland.rs b/src/wayland.rs deleted file mode 100644 index a87e3b16..00000000 --- a/src/wayland.rs +++ /dev/null @@ -1,632 +0,0 @@ -// TODO: scale-factor? - -use derivative::Derivative; -use gdk4_wayland::prelude::*; -use gtk4::{ - cairo, gdk, - glib::{ - self, clone, - subclass::{prelude::*, Signal}, - translate::*, - ParamFlags, ParamSpec, ParamSpecBoolean, SignalHandlerId, Value, - }, - gsk::{self, traits::GskRendererExt}, - prelude::*, - subclass::prelude::*, -}; -use once_cell::sync::Lazy; -use std::{ - cell::{Cell, RefCell}, - os::raw::c_int, - ptr, - rc::Rc, -}; -use wayland_client::{event_enum, sys::client::wl_proxy, Filter, GlobalManager, Main, Proxy}; -use wayland_protocols::{ - wlr::unstable::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}, - xdg_shell::client::xdg_popup, -}; - -use crate::{deref_cell::DerefCell, wayland_custom_surface::WaylandCustomSurface}; - -pub use wayland_protocols::wlr::unstable::layer_shell::v1::client::{ - zwlr_layer_shell_v1::Layer, - zwlr_layer_surface_v1::{Anchor, KeyboardInteractivity}, -}; - -event_enum!( - Events | - LayerSurface => zwlr_layer_surface_v1::ZwlrLayerSurfaceV1 -); - -struct CosmicWaylandDisplay { - event_queue: RefCell, - wayland_display: wayland_client::Display, - wlr_layer_shell: Option>, -} - -impl CosmicWaylandDisplay { - fn for_display(display: &gdk4_wayland::WaylandDisplay) -> Rc { - const DATA_KEY: &str = "cosmic-wayland-display"; - - // `GdkWaylandDisplay` already associated with a `CosmicWaylandDisplay` - if let Some(data) = unsafe { display.data::>(DATA_KEY) } { - return unsafe { data.as_ref() }.clone(); - } - - let wayland_display = unsafe { - wayland_client::Display::from_external_display(display.wl_display().c_ptr() as *mut _) - }; // XXX is this sound? - - let mut event_queue = wayland_display.create_event_queue(); - let attached_display = wayland_display.attach(event_queue.token()); - let globals = GlobalManager::new(&attached_display); - - event_queue.sync_roundtrip(&mut (), |_, _, _| {}).unwrap(); - - let wlr_layer_shell = globals - .instantiate_exact::(1) - .ok(); - - let cosmic_wayland_display = Rc::new(Self { - event_queue: RefCell::new(event_queue), - wayland_display, - wlr_layer_shell, - }); - - unsafe { display.set_data(DATA_KEY, cosmic_wayland_display.clone()) }; - - // XXX Efficient way to poll? - // XXX unwrap? - glib::idle_add_local( - clone!(@weak cosmic_wayland_display => @default-return Continue(false), move || { - cosmic_wayland_display.wayland_display.flush().unwrap(); - let mut event_queue = cosmic_wayland_display.event_queue.borrow_mut(); - if let Some(guard) = event_queue.prepare_read() { - guard.read_events().unwrap(); - } - event_queue.dispatch_pending(&mut (), |_, _, _| {}).unwrap(); - Continue(true) - }), - ); - - cosmic_wayland_display - } -} - -#[derive(Derivative)] -#[derivative(Default)] -pub struct LayerShellWindowInner { - display: DerefCell, - surface: RefCell>, - renderer: RefCell>, - wlr_layer_surface: RefCell>>, - constraint_solver: DerefCell, - child: RefCell>, - monitor: DerefCell>, - #[derivative(Default(value = "Cell::new(Layer::Background)"))] - layer: Cell, - #[derivative(Default(value = "Cell::new(Anchor::empty())"))] - anchor: Cell, - exclusive_zone: Cell, - margin: Cell<(i32, i32, i32, i32)>, - #[derivative(Default(value = "Cell::new(KeyboardInteractivity::None)"))] - keyboard_interactivity: Cell, - namespace: DerefCell, - focus_widget: RefCell>, - is_active: Rc>, -} - -#[glib::object_subclass] -impl ObjectSubclass for LayerShellWindowInner { - const NAME: &'static str = "S76CosmicLayerShellWindow"; - type ParentType = gtk4::Widget; - type Interfaces = (gtk4::Native, gtk4::Root); - type Type = LayerShellWindow; -} - -impl ObjectImpl for LayerShellWindowInner { - fn constructed(&self, obj: &Self::Type) { - self.display - .set(gdk::Display::default().unwrap().downcast().unwrap()); // XXX any issue unwrapping? - self.constraint_solver.set(glib::Object::new(&[]).unwrap()); - - obj.add_css_class("background"); - } - - fn properties() -> &'static [ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ParamSpecBoolean::new( - // Name - "is-active", - // Nickname - "is-active", - // Short description - "Whether the window has keyboard focus", - // Default value - false, - // The property can be read and written to - ParamFlags::READWRITE, - )] - }); - PROPERTIES.as_ref() - } - - fn set_property(&self, _obj: &Self::Type, _id: usize, value: &Value, pspec: &ParamSpec) { - match pspec.name() { - "is-active" => { - let is_active = value - .get() - .expect("The is_active property needs to be of type `bool`"); - self.is_active.replace(is_active); - } - _ => unimplemented!(), - } - } - - fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> Value { - match pspec.name() { - "is-active" => self.is_active.get().to_value(), - _ => unimplemented!(), - } - } - - fn signals() -> &'static [Signal] { - static SIGNALS: Lazy> = Lazy::new(|| { - vec![Signal::builder("is-active-notify") - .param_types(&[bool::static_type().into()]) - .build()] - }); - SIGNALS.as_ref() - } -} - -impl WidgetImpl for LayerShellWindowInner { - fn realize(&self, widget: &Self::Type) { - let surface = WaylandCustomSurface::new(&*self.display); - surface.set_get_popup_func(Some(Box::new(clone!(@strong widget => move |_surface, popup| { - if let Some(wlr_layer_surface) = widget.inner().wlr_layer_surface.borrow().as_ref() { - let xdg_popup: xdg_popup::XdgPopup = - unsafe { Proxy::from_c_ptr(gdk_wayland_popup_get_xdg_popup(popup.to_glib_none().0)).into() }; - wlr_layer_surface.get_popup(&xdg_popup); - } - true - })))); - widget.layer_shell_init(&surface); - - let widget_ptr: *mut _ = widget.to_glib_none().0; - let surface_ptr: *mut _ = surface.to_glib_none().0; - unsafe { gdk_surface_set_widget(surface_ptr as *mut _, widget_ptr as *mut _) }; - *self.surface.borrow_mut() = Some(surface.clone()); - surface.connect_render(move |_surface, region| { - unsafe { - gtk_widget_render( - widget_ptr as *mut _, - surface_ptr as *mut _, - region.to_glib_none().0, - ) - }; - true - }); - surface.connect_event( - glib::clone!(@weak widget => @default-return true, move |_, event| { - if event.event_type() == gdk::EventType::FocusChange { - let is_active = event.downcast_ref::().unwrap().is_in(); - widget.set_property("is-active", is_active); - widget.emit_by_name::<()>("is-active-notify", &[&is_active]); - } - unsafe { gtk_main_do_event(event.to_glib_none().0) }; - true - }), - ); - - self.parent_realize(widget); - - *self.renderer.borrow_mut() = - Some(gsk::Renderer::for_surface(surface.upcast_ref()).unwrap()); // XXX unwrap? - - unsafe { gtk4::ffi::gtk_native_realize(widget_ptr as *mut _) }; - } - - fn unrealize(&self, widget: &Self::Type) { - let widget_ptr: *mut Self::Instance = widget.to_glib_none().0; - - unsafe { gtk4::ffi::gtk_native_unrealize(widget_ptr as *mut _) }; - - self.parent_unrealize(widget); - - if let Some(renderer) = self.renderer.borrow_mut().take() { - renderer.unrealize(); - } - - if let Some(surface) = self.surface.borrow().as_ref() { - let surface_ptr: *mut _ = surface.to_glib_none().0; - unsafe { gdk_surface_set_widget(surface_ptr as *mut _, ptr::null_mut()) }; - } - } - - fn map(&self, widget: &Self::Type) { - if let Some(surface) = self.surface.borrow().as_ref() { - let width = widget.measure(gtk4::Orientation::Horizontal, -1).1; - let height = widget.measure(gtk4::Orientation::Vertical, width).1; - widget.set_size(width as u32, height as u32); - surface.present(width, height); - } - - self.parent_map(widget); - - if let Some(child) = self.child.borrow().as_ref() { - child.map(); - } - } - - fn unmap(&self, widget: &Self::Type) { - self.parent_unmap(widget); - - if let Some(surface) = self.surface.borrow().as_ref() { - surface.hide(); - } - - if let Some(child) = self.child.borrow().as_ref() { - child.unmap(); - } - } - - fn measure( - &self, - _widget: &Self::Type, - orientation: gtk4::Orientation, - for_size: i32, - ) -> (i32, i32, i32, i32) { - if let Some(child) = self.child.borrow().as_ref() { - child.measure(orientation, for_size) - } else { - (0, 0, 0, 0) - } - } - - fn size_allocate(&self, _widget: &Self::Type, width: i32, height: i32, baseline: i32) { - if let Some(child) = self.child.borrow().as_ref() { - child.allocate(width, height, baseline, None) - } - } - - fn show(&self, widget: &Self::Type) { - WidgetExt::realize(widget); - self.parent_show(widget); - widget.map(); - } - - fn hide(&self, widget: &Self::Type) { - self.parent_hide(widget); - widget.unmap(); - } -} - -// TODO: Move into gtk4-rs when support merged/released in gtk -unsafe impl IsImplementable for gtk4::Native { - fn interface_init(iface: &mut glib::Interface) { - let iface = unsafe { &mut *(iface as *mut _ as *mut GtkNativeInterface) }; - iface.get_surface = Some(get_surface); - iface.get_renderer = Some(get_renderer); - iface.get_surface_transform = Some(get_surface_transform); - iface.layout = Some(layout); - } - - fn instance_init(_instance: &mut glib::subclass::InitializingObject) {} -} - -// TODO: Move into gtk4-rs when support merged/released in gtk -unsafe impl IsImplementable for gtk4::Root { - fn interface_init(iface: &mut glib::Interface) { - let iface = unsafe { &mut *(iface as *mut _ as *mut GtkRootInterface) }; - iface.get_display = Some(get_display); - iface.get_constraint_solver = Some(get_constraint_solver); - iface.get_focus = Some(get_focus); - iface.set_focus = Some(set_focus); - } - - fn instance_init(_instance: &mut glib::subclass::InitializingObject) {} -} - -glib::wrapper! { - pub struct LayerShellWindow(ObjectSubclass) - @extends gtk4::Widget, @implements gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Native, gtk4::Root; -} -// TODO handle configure/destroy -// TODO presumably call destroy() when appropriate? -// What do wayland-client types do when associated connection is gone? Panic? UB? -impl LayerShellWindow { - pub fn new( - monitor: Option<&gdk4_wayland::WaylandMonitor>, - layer: Layer, - namespace: &str, - ) -> Self { - let obj: Self = glib::Object::new(&[]).unwrap(); - obj.inner().monitor.set(monitor.cloned()); - obj.inner().layer.set(layer); - obj.inner().namespace.set(namespace.to_string()); - obj - } - - fn inner(&self) -> &LayerShellWindowInner { - LayerShellWindowInner::from_instance(self) - } - - pub fn set_child>(&self, w: Option<&T>) { - let mut child = self.inner().child.borrow_mut(); - if let Some(child) = child.take() { - child.unparent(); - } - if let Some(w) = w { - w.set_parent(self); - } - *child = w.map(|x| x.clone().upcast()); - } - - pub fn is_active(&self) -> bool { - self.property::("is-active") - } - - pub fn connect_is_active_notify(&self, f: F) -> SignalHandlerId { - self.connect_local("is-active-notify", false, move |args| { - f(&args[0].get::().unwrap()); - None - }) - } - - fn layer_shell_init(&self, surface: &WaylandCustomSurface) { - let width = self.measure(gtk4::Orientation::Horizontal, -1).1; - let height = self.measure(gtk4::Orientation::Vertical, width).1; - // XXX needed for wl_surface to exist - surface.present(width, height); - - let wl_surface = surface.wl_surface(); - - let cosmic_wayland_display = CosmicWaylandDisplay::for_display(&*self.inner().display); - let wlr_layer_shell = match cosmic_wayland_display.wlr_layer_shell.as_ref() { - Some(wlr_layer_shell) => wlr_layer_shell, - None => { - eprintln!("Error: Layer shell not supported by compositor"); - return; - } - }; - - let output = self.inner().monitor.as_ref().map(|x| x.wl_output()); - let layer = self.layer(); - let namespace = self.namespace().to_string(); - let wlr_layer_surface = - wlr_layer_shell.get_layer_surface(&wl_surface, output.as_ref(), layer, namespace); - - wlr_layer_surface.set_anchor(self.anchor()); - wlr_layer_surface.set_exclusive_zone(self.exclusive_zone()); - let margin = self.margin(); - wlr_layer_surface.set_margin(margin.0, margin.1, margin.2, margin.3); - wlr_layer_surface.set_keyboard_interactivity(self.keyboard_interactivity()); - wlr_layer_surface.set_size(width as u32, height as u32); - - let filter = Filter::new( - clone!(@strong self as self_ => move |event, _, _| match event { - Events::LayerSurface { event, object } => match event { - zwlr_layer_surface_v1::Event::Configure { - serial, - width: _, - height: _, - } => { - // TODO: should size window to match `width`/`height` - object.ack_configure(serial); - } - zwlr_layer_surface_v1::Event::Closed => {} - _ => {} - }, - }), - ); - wlr_layer_surface.assign(filter); - - wl_surface.commit(); - - cosmic_wayland_display - .event_queue - .borrow_mut() - .sync_roundtrip(&mut (), |_, _, _| {}) - .unwrap(); - - *self.inner().wlr_layer_surface.borrow_mut() = Some(wlr_layer_surface); - } - - fn set_size(&self, width: u32, height: u32) { - if let Some(wlr_layer_surface) = self.inner().wlr_layer_surface.borrow().as_ref() { - wlr_layer_surface.set_size(width, height); - }; - } - - pub fn anchor(&self) -> Anchor { - self.inner().anchor.get() - } - - pub fn set_anchor(&self, anchor: Anchor) { - if let Some(wlr_layer_surface) = self.inner().wlr_layer_surface.borrow().as_ref() { - wlr_layer_surface.set_anchor(anchor); - }; - self.inner().anchor.set(anchor); - } - - pub fn exclusive_zone(&self) -> i32 { - self.inner().exclusive_zone.get() - } - - pub fn set_exclusive_zone(&self, zone: i32) { - if let Some(wlr_layer_surface) = self.inner().wlr_layer_surface.borrow().as_ref() { - wlr_layer_surface.set_exclusive_zone(zone); - }; - self.inner().exclusive_zone.set(zone); - } - - pub fn margin(&self) -> (i32, i32, i32, i32) { - self.inner().margin.get() - } - - pub fn set_margin(&self, top: i32, right: i32, bottom: i32, left: i32) { - if let Some(wlr_layer_surface) = self.inner().wlr_layer_surface.borrow().as_ref() { - wlr_layer_surface.set_margin(top, right, bottom, left); - }; - self.inner().margin.set((top, right, bottom, left)); - } - - pub fn keyboard_interactivity(&self) -> KeyboardInteractivity { - self.inner().keyboard_interactivity.get() - } - - pub fn set_keyboard_interactivity(&self, interactivity: KeyboardInteractivity) { - if let Some(wlr_layer_surface) = self.inner().wlr_layer_surface.borrow().as_ref() { - wlr_layer_surface.set_keyboard_interactivity(interactivity); - }; - self.inner().keyboard_interactivity.set(interactivity); - } - - pub fn layer(&self) -> Layer { - self.inner().layer.get() - } - - pub fn set_layer(&self, layer: Layer) { - if let Some(wlr_layer_surface) = self.inner().wlr_layer_surface.borrow().as_ref() { - wlr_layer_surface.set_layer(layer); - }; - self.inner().layer.set(layer); - } - - pub fn namespace(&self) -> &str { - self.inner().namespace.as_str() - } -} - -pub struct GtkConstraintSolver { - _private: [u8; 0], -} - -// XXX needs to be public in gtk -#[link(name = "gtk-4")] -extern "C" { - pub fn gtk_constraint_solver_get_type() -> glib::ffi::GType; - - pub fn gdk_surface_set_widget(surface: *mut gdk::ffi::GdkSurface, widget: glib::ffi::gpointer); - - pub fn _gtk_widget_set_visible_flag( - widget: *mut gtk4::ffi::GtkWidget, - visible: glib::ffi::gboolean, - ); - - pub fn gtk_widget_render( - widget: *mut gtk4::ffi::GtkWidget, - surface: *mut gdk::ffi::GdkSurface, - region: *const cairo::ffi::cairo_region_t, - ); - - pub fn gtk_main_do_event(event: *mut gdk::ffi::GdkEvent); - - // Added API - pub fn gdk_wayland_popup_get_xdg_popup( - popup: *mut gdk4_wayland::ffi::GdkWaylandPopup, - ) -> *mut wl_proxy; -} - -glib::wrapper! { - pub struct ConstraintSolver(Object); - - match fn { - type_ => || gtk_constraint_solver_get_type(), - } -} - -pub struct GtkNativeInterface { - pub g_iface: gobject_sys::GTypeInterface, - pub get_surface: - Option *mut gdk::ffi::GdkSurface>, - pub get_renderer: Option< - unsafe extern "C" fn(self_: *mut gtk4::ffi::GtkNative) -> *mut gsk::ffi::GskRenderer, - >, - pub get_surface_transform: - Option, - pub layout: - Option, -} - -pub struct GtkRootInterface { - pub g_iface: gobject_sys::GTypeInterface, - pub get_display: - Option *mut gdk::ffi::GdkDisplay>, - pub get_constraint_solver: - Option *mut GtkConstraintSolver>, - pub get_focus: - Option *mut gtk4::ffi::GtkWidget>, - pub set_focus: Option< - unsafe extern "C" fn(self_: *mut gtk4::ffi::GtkRoot, focus: *mut gtk4::ffi::GtkWidget), - >, -} - -unsafe extern "C" fn get_surface(native: *mut gtk4::ffi::GtkNative) -> *mut gdk::ffi::GdkSurface { - let instance = &*(native as *mut ::Instance); - let imp = instance.imp(); - imp.surface.borrow().as_ref().map_or(ptr::null_mut(), |x| { - x.upcast_ref::().to_glib_none().0 - }) -} - -unsafe extern "C" fn get_renderer(native: *mut gtk4::ffi::GtkNative) -> *mut gsk::ffi::GskRenderer { - let instance = &*(native as *mut ::Instance); - let imp = instance.imp(); - imp.renderer - .borrow() - .as_ref() - .map_or(ptr::null_mut(), |x| x.to_glib_none().0) -} - -unsafe extern "C" fn get_surface_transform( - _native: *mut gtk4::ffi::GtkNative, - x: *mut f64, - y: *mut f64, -) { - // TODO: add css logic like `GtkWindow` has - - *x = 0.; - *y = 0.; -} - -unsafe extern "C" fn layout(native: *mut gtk4::ffi::GtkNative, width: c_int, height: c_int) { - // TODO: `GtkWindow` has more here - gtk4::ffi::gtk_widget_allocate(native as *mut _, width, height, -1, ptr::null_mut()); -} - -unsafe extern "C" fn get_display(root: *mut gtk4::ffi::GtkRoot) -> *mut gdk::ffi::GdkDisplay { - let instance = &*(root as *mut ::Instance); - let imp = instance.imp(); - imp.display.upcast_ref::().to_glib_none().0 -} - -unsafe extern "C" fn get_constraint_solver( - root: *mut gtk4::ffi::GtkRoot, -) -> *mut GtkConstraintSolver { - let instance = &*(root as *mut ::Instance); - let imp = instance.imp(); - imp.constraint_solver.to_glib_none().0 -} - -unsafe extern "C" fn get_focus(root: *mut gtk4::ffi::GtkRoot) -> *mut gtk4::ffi::GtkWidget { - let instance = &*(root as *mut ::Instance); - let imp = instance.imp(); - imp.focus_widget - .borrow() - .as_ref() - .map_or(ptr::null_mut(), |x| x.to_glib_none().0) -} - -unsafe extern "C" fn set_focus(root: *mut gtk4::ffi::GtkRoot, focus: *mut gtk4::ffi::GtkWidget) { - // TODO: `GtkWindow` does more here - let instance = &*(root as *mut ::Instance); - let imp = instance.imp(); - *imp.focus_widget.borrow_mut() = if focus.is_null() { - None - } else { - Some(gtk4::Widget::from_glib_none(focus)) - }; -} diff --git a/src/wayland_custom_surface.rs b/src/wayland_custom_surface.rs deleted file mode 100644 index fceb4cb5..00000000 --- a/src/wayland_custom_surface.rs +++ /dev/null @@ -1,144 +0,0 @@ -use gdk4_wayland::{WaylandDisplay, WaylandPopup}; -use gtk4::{ - gdk, - glib::{self, translate::*}, -}; -use std::boxed::Box as Box_; -use std::fmt; - -mod ffi { - use gdk4_wayland::ffi::{GdkWaylandDisplay, GdkWaylandPopup}; - use gtk4::{ - gdk::ffi::GdkFrameClock, - glib::ffi::{gboolean, gpointer, GDestroyNotify, GType}, - }; - use std::os::raw::c_int; - - pub type GdkWaylandCustomSurfaceGetPopupFunc = Option< - unsafe extern "C" fn( - *mut GdkWaylandCustomSurface, - *mut GdkWaylandPopup, - gpointer, - ) -> gboolean, - >; - - #[repr(C)] - pub struct GdkWaylandCustomSurface { - _data: [u8; 0], - _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>, - } - - impl ::std::fmt::Debug for GdkWaylandCustomSurface { - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - f.debug_struct(&format!("GdkWaylandCustomSurface @ {:p}", self)) - .finish() - } - } - - extern "C" { - pub fn gdk_wayland_custom_surface_get_type() -> GType; - - pub fn gdk_wayland_custom_surface_new( - display: *mut GdkWaylandDisplay, - ) -> *mut GdkWaylandCustomSurface; - - pub fn gdk_wayland_custom_surface_present( - custom_surface: *mut GdkWaylandCustomSurface, - width: c_int, - height: c_int, - ); - - pub fn gdk_wayland_custom_surface_set_get_popup_func( - custom_surface: *mut GdkWaylandCustomSurface, - get_popup_func: GdkWaylandCustomSurfaceGetPopupFunc, - user_data: gpointer, - destroy: GDestroyNotify, - ); - - pub fn _gdk_frame_clock_idle_new() -> *mut GdkFrameClock; - } -} - -glib::wrapper! { - #[doc(alias = "GdkWaylandCustomSurface")] - pub struct WaylandCustomSurface(Object) @extends gdk4_wayland::WaylandSurface, gdk::Surface; - - match fn { - type_ => || ffi::gdk_wayland_custom_surface_get_type(), - } -} - -impl WaylandCustomSurface { - #[doc(alias = "gdk_wayland_custom_surface_new")] - pub fn new(display: &WaylandDisplay) -> WaylandCustomSurface { - unsafe { - from_glib_full(ffi::gdk_wayland_custom_surface_new( - display.to_glib_none().0, - )) - } - } - - #[doc(alias = "gdk_wayland_custom_surface_present")] - pub fn present(&self, width: i32, height: i32) { - unsafe { - ffi::gdk_wayland_custom_surface_present(self.to_glib_none().0, width, height); - } - } - - #[doc(alias = "gdk_wayland_custom_surface_set_get_popup_func")] - pub fn set_get_popup_func( - &self, - get_popup_func: Option< - Box_ bool + 'static>, - >, - ) { - let get_popup_func_data: Box_< - Option bool + 'static>>, - > = Box_::new(get_popup_func); - unsafe extern "C" fn get_popup_func_func( - custom_surface: *mut ffi::GdkWaylandCustomSurface, - popup: *mut gdk4_wayland::ffi::GdkWaylandPopup, - user_data: glib::ffi::gpointer, - ) -> glib::ffi::gboolean { - let custom_surface = from_glib_borrow(custom_surface); - let popup = from_glib_borrow(popup); - let callback: &Option< - Box_ bool + 'static>, - > = &*(user_data as *mut _); - let res = if let Some(ref callback) = *callback { - callback(&custom_surface, &popup) - } else { - panic!("cannot get closure...") - }; - res.into_glib() - } - let get_popup_func = if get_popup_func_data.is_some() { - Some(get_popup_func_func as _) - } else { - None - }; - unsafe extern "C" fn destroy_func(data: glib::ffi::gpointer) { - let _callback: Box_< - Option bool + 'static>>, - > = Box_::from_raw(data as *mut _); - } - let destroy_call3 = Some(destroy_func as _); - let super_callback0: Box_< - Option bool + 'static>>, - > = get_popup_func_data; - unsafe { - ffi::gdk_wayland_custom_surface_set_get_popup_func( - self.to_glib_none().0, - get_popup_func, - Box_::into_raw(super_callback0) as *mut _, - destroy_call3, - ); - } - } -} - -impl fmt::Display for WaylandCustomSurface { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("WaylandCustomSurface") - } -} diff --git a/src/widget/button.rs b/src/widget/button.rs new file mode 100644 index 00000000..7193077e --- /dev/null +++ b/src/widget/button.rs @@ -0,0 +1,50 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{theme, Element, Renderer}; +use iced::widget; + +/// A button widget with COSMIC styling +#[must_use] +pub const fn button(style: theme::Button) -> Button { + Button { style, message: None } +} + +/// A button widget with COSMIC styling +pub struct Button { + style: theme::Button, + message: Option, +} + +impl Button { + /// The message to emit on button press. + #[must_use] + pub fn on_press(mut self, message: Message) -> Self { + self.message = Some(message); + self + } + + /// A button with an icon. + pub fn icon(self, style: theme::Svg, icon: &str, size: u16) -> widget::Button { + self.custom(vec![super::icon(icon, size).style(style).into()]) + } + + /// A button with text. + pub fn text(self, text: &str) -> widget::Button { + self.custom(vec![text.into()]) + } + + /// A custom button that has the desired default spacing and padding. + pub fn custom(self, children: Vec>) -> widget::Button { + let button = widget::button(widget::row(children).spacing(8)) + .style(self.style) + .padding([8, 16]); + + if let Some(message) = self.message { + button.on_press(message) + } else { + button + } + } +} + diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs new file mode 100644 index 00000000..1c6b26e6 --- /dev/null +++ b/src/widget/header_bar.rs @@ -0,0 +1,129 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use apply::Apply; +use derive_setters::Setters; +use iced::{self, widget, Length}; +use crate::{theme, Element}; + +#[must_use] +pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { + HeaderBar { + title: "", + on_close: None, + on_drag: None, + on_maximize: None, + on_minimize: None, + start: None, + center: None, + end: None, + } +} + +#[derive(Setters)] +pub struct HeaderBar<'a, Message> { + title: &'a str, + #[setters(strip_option)] + on_close: Option, + #[setters(strip_option)] + on_drag: Option, + #[setters(strip_option)] + on_maximize: Option, + #[setters(strip_option)] + on_minimize: Option, + #[setters(strip_option)] + start: Option>, + #[setters(strip_option)] + center: Option>, + #[setters(strip_option)] + end: Option> +} + +impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { + /// Converts the headerbar builder into an Iced element. + pub fn into_element(mut self) -> Element<'a, Message> { + let mut packed: Vec> = Vec::with_capacity(4); + + if let Some(start) = self.start.take() { + packed.push(widget::container(start).align_x(iced::alignment::Horizontal::Left).into()); + } + + packed.push(if let Some(center) = self.center.take() { + widget::container(center).align_x(iced::alignment::Horizontal::Center).into() + } else { + self.title_widget() + }); + + packed.push(if let Some(end) = self.end.take() { + widget::row(vec![end, self.window_controls()]) + .apply(widget::container) + .align_x(iced::alignment::Horizontal::Right) + .into() + } else { + self.window_controls() + }); + + let mut widget = widget::row(packed) + .height(Length::Units(50)) + .padding(10) + .apply(widget::event_container) + .center_y(); + + if let Some(message) = self.on_drag.clone() { + widget = widget.on_press(message); + } + + if let Some(message) = self.on_maximize.clone() { + widget = widget.on_release(message); + } + + widget.into() + } + + fn title_widget(&self) -> Element<'a, Message> { + widget::container(widget::text(self.title)) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + /// Creates the widget for window controls. + fn window_controls(&mut self) -> Element<'a, Message> { + let mut widgets: Vec> = Vec::with_capacity(3); + + let icon = |name, size, on_press| { + super::icon(name, size) + .style(crate::theme::Svg::SymbolicActive) + .apply(iced::widget::button) + .style(theme::Button::Text) + .on_press(on_press) + }; + + if let Some(message) = self.on_minimize.take() { + widgets.push(icon("window-minimize-symbolic", 16, message).into()); + } + + if let Some(message) = self.on_maximize.take() { + widgets.push(icon("window-maximize-symbolic", 16, message).into()); + } + + if let Some(message) = self.on_close.take() { + widgets.push(icon("window-close-symbolic", 16, message).into()); + } + + widget::row(widgets) + .spacing(8) + .apply(widget::container) + .height(Length::Fill) + .center_y() + .into() + } +} + +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(headerbar: HeaderBar<'a, Message>) -> Self { + headerbar.into_element() + } +} diff --git a/src/widget/icon.rs b/src/widget/icon.rs new file mode 100644 index 00000000..0d5311c1 --- /dev/null +++ b/src/widget/icon.rs @@ -0,0 +1,97 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Lazily-generated SVG icon widget for Iced. + +use iced::{ + widget::{svg, Image}, + Length, ContentFit, +}; +use std::borrow::Cow; +use std::hash::Hash; +use std::rc::Rc; +use derive_setters::Setters; +use crate::{Element, Renderer}; + +/// A lazily-generated SVG icon. +#[derive(Hash, Setters)] +pub struct Icon<'a> { + #[setters(skip)] + name: Cow<'a, str>, + #[setters(into)] + theme: Cow<'a, str>, + style: crate::theme::Svg, + size: u16, + #[setters(strip_option)] + content_fit: Option, + #[setters(strip_option)] + width: Option, + #[setters(strip_option)] + height: Option, +} + +pub fn image_icon(name: &str, size: u16) -> Option { + freedesktop_icons::lookup(name) + .with_size(size) + .with_cache() + .find() + .map(|path| { + Image::new(path) + .width(Length::Units(size)) + .height(Length::Units(size)) + }) +} +/// A lazily-generated SVG icon. +#[must_use] +pub fn icon<'a>(name: impl Into>, size: u16) -> Icon<'a> { + Icon { + content_fit: None, + height: None, + name: name.into(), + size, + style: crate::theme::Svg::default(), + theme: Cow::Borrowed("Pop"), + width: None, + } +} + +impl<'a> Icon<'a> { + #[must_use] + fn into_svg(self) -> Element<'a, Message> { + let svg = Rc::new(self); + let svg_clone = Rc::clone(&svg); + + iced_lazy::lazy(svg_clone, move || -> Element { + let icon = freedesktop_icons::lookup(&svg.name) + .with_size(svg.size) + .with_theme(&svg.theme) + .with_cache() + .force_svg() + .find(); + + let handle = if let Some(path) = icon { + svg::Handle::from_path(path) + } else { + eprintln!("icon '{}' size {} not found", svg.name, svg.size); + svg::Handle::from_memory(Vec::new()) + }; + + let mut widget = svg::Svg::::new(handle) + .style(svg.style) + .width(svg.width.unwrap_or(Length::Units(svg.size))) + .height(svg.height.unwrap_or(Length::Units(svg.size))); + + if let Some(content_fit) = svg.content_fit { + widget = widget.content_fit(content_fit); + } + + widget.into() + }).into() + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(icon: Icon<'a>) -> Self { + icon.into_svg::() + } +} diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs new file mode 100644 index 00000000..f77f593d --- /dev/null +++ b/src/widget/list/column.rs @@ -0,0 +1,65 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use apply::Apply; +use crate::{Element, theme}; +use crate::widget::horizontal_rule; +use iced::{Background, Color}; + +#[must_use] +pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { + ListColumn::default() +} + +pub struct ListColumn<'a, Message> { + children: Vec>, +} + +impl<'a, Message: 'static> Default for ListColumn<'a, Message> { + fn default() -> Self { + Self { children: Vec::with_capacity(4) } + } +} + +impl<'a, Message: 'static> ListColumn<'a, Message> { + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn add(mut self, item: impl Into>) -> Self { + if !self.children.is_empty() { + self.children.push(horizontal_rule(12).into()); + } + + self.children.push(item.into()); + self + } + + #[must_use] + pub fn into_element(self) -> Element<'a, Message> { + iced::widget::column(self.children) + .spacing(12) + .apply(iced::widget::container) + .padding([12, 16]) + .style(theme::Container::Custom(style)) + .into() + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(column: ListColumn<'a, Message>) -> Self { + column.into_element() + } +} + +fn style(theme: &crate::Theme) -> iced::widget::container::Appearance { + let cosmic = &theme.cosmic().primary; + iced::widget::container::Appearance { + text_color: Some(cosmic.on.into()), + background: Some(Background::Color(cosmic.base.into())), + border_radius: 8.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + } +} \ No newline at end of file diff --git a/src/widget/list/item.rs b/src/widget/list/item.rs new file mode 100644 index 00000000..8eeae958 --- /dev/null +++ b/src/widget/list/item.rs @@ -0,0 +1,2 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 diff --git a/src/widget/list/mod.rs b/src/widget/list/mod.rs new file mode 100644 index 00000000..646d242b --- /dev/null +++ b/src/widget/list/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +mod column; +// mod item; + +pub use self::column::{ListColumn, list_column}; +// pub use self::item::{ListItem, list_item}; \ No newline at end of file diff --git a/src/widget/mod.rs b/src/widget/mod.rs new file mode 100644 index 00000000..6e1aa053 --- /dev/null +++ b/src/widget/mod.rs @@ -0,0 +1,34 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +mod button; +pub use button::*; + +mod header_bar; +pub use header_bar::{HeaderBar, header_bar}; + +mod icon; +pub use self::icon::{Icon, icon}; + +pub mod list; +pub use self::list::*; + +pub mod nav_button; +pub use self::nav_button::{NavButton, nav_button}; + +pub mod navigation; +pub use navigation::*; + +mod toggler; +pub use toggler::toggler; + +pub mod settings; + +mod scrollable; +pub use scrollable::*; + +pub mod separator; + +pub mod popup; +pub use popup::*; +pub use separator::{horizontal_rule, vertical_rule}; diff --git a/src/widget/nav_button.rs b/src/widget/nav_button.rs new file mode 100644 index 00000000..f2711845 --- /dev/null +++ b/src/widget/nav_button.rs @@ -0,0 +1,61 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use apply::Apply; +use derive_setters::Setters; +use iced::{alignment::Vertical, Length}; +use crate::{Element, theme}; + +#[derive(Setters)] +pub struct NavButton<'a, Message> { + title: &'a str, + sidebar_active: bool, + #[setters(strip_option)] + on_sidebar_toggled: Option, +} + +#[must_use] +pub fn nav_button(title: &str) -> NavButton { + NavButton { + title, + sidebar_active: false, + on_sidebar_toggled: None, + } +} + +impl<'a, Message: 'static + Clone> From> for Element<'a, Message> { + fn from(nav_button: NavButton<'a, Message>) -> Self { + let text = iced::widget::text(&nav_button.title) + .style(theme::Text::Accent) + .vertical_alignment(Vertical::Center) + .width(Length::Shrink) + .height(Length::Fill); + + let icon = super::icon( + if nav_button.sidebar_active { + "go-previous-symbolic" + } else { + "go-next-symbolic" + }, + 24, + ) + .style(theme::Svg::SymbolicActive) + .width(Length::Units(24)) + .height(Length::Fill); + + let mut widget = iced::widget::row!(text, crate::widget::vertical_rule(4), icon) + .padding(4) + .spacing(4) + .apply(iced::widget::button) + .style(theme::Button::Secondary); + + if let Some(message) = nav_button.on_sidebar_toggled.clone() { + widget = widget.on_press(message); + } + + widget.apply(iced::widget::container) + .center_y() + .height(Length::Fill) + .into() + } +} \ No newline at end of file diff --git a/src/widget/navigation/macros.rs b/src/widget/navigation/macros.rs new file mode 100644 index 00000000..0c1f6a03 --- /dev/null +++ b/src/widget/navigation/macros.rs @@ -0,0 +1,47 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +pub mod nav_bar { + use crate::Theme; + use iced::{widget, Background, Color}; + + #[macro_export] + macro_rules! nav_button { + ($icon: expr, $title:expr, $condensed:expr) => {{ + if $condensed { + $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), + ) + } + }}; + } + + pub fn nav_bar_sections_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.0, + border_color: Color::TRANSPARENT, + } + } + + pub fn nav_bar_pages_style(theme: &Theme) -> widget::container::Appearance { + let primary = &theme.cosmic().primary; + let secondary = &theme.cosmic().secondary; + widget::container::Appearance { + text_color: Some(primary.on.into()), + background: Some(Background::Color(secondary.component.base.into())), + border_radius: 8.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + + pub use nav_button; +} diff --git a/src/widget/navigation/mod.rs b/src/widget/navigation/mod.rs new file mode 100644 index 00000000..f8fcdecd --- /dev/null +++ b/src/widget/navigation/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +pub mod navbar; +pub use navbar::*; + +pub mod macros; +pub use macros::*; diff --git a/src/widget/navigation/navbar.rs b/src/widget/navigation/navbar.rs new file mode 100644 index 00000000..0c4138f9 --- /dev/null +++ b/src/widget/navigation/navbar.rs @@ -0,0 +1,231 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::widget::nav_bar::{nav_bar_pages_style, nav_bar_sections_style}; +use crate::widget::{icon, scrollable}; +use crate::{theme, Renderer, Theme}; +use derive_setters::Setters; +use iced::{Background, Length}; +use iced_lazy::Component; +use iced_native::widget::{button, column, container, text}; +use iced_native::{row, Alignment, Element}; +use iced_style::button::Appearance; +use std::collections::BTreeMap; + +#[derive(Setters, Default)] +pub struct NavBar<'a, Message> { + source: BTreeMap>, + active: bool, + condensed: bool, + on_page_selected: Option Message + 'a>>, +} + +impl<'a, Message> NavBar<'a, Message> { + pub fn new() -> Self { + Self { + source: Default::default(), + active: false, + condensed: false, + on_page_selected: None, + } + } +} + +pub fn nav_bar<'a, Message>() -> NavBar<'a, Message> { + NavBar::new() +} + +#[derive(Setters, Clone, Default, PartialOrd, Ord, PartialEq, Eq, Hash)] +pub struct NavBarSection { + #[setters(into)] + title: String, + #[setters(into)] + icon: String, +} + +impl NavBarSection { + pub fn new() -> Self { + Self::default() + } +} + +pub fn nav_bar_section() -> NavBarSection { + NavBarSection::new() +} + +#[derive(Default, Clone, Setters, PartialOrd, Ord, PartialEq, Eq)] +pub struct NavBarPage { + #[setters(into)] + title: String, +} + +impl NavBarPage { + pub fn new() -> Self { + Self { + title: String::new(), + } + } +} + +pub fn nav_bar_page(title: &str) -> NavBarPage { + let mut page = NavBarPage::new(); + page.title = title.to_string(); + page +} + +#[derive(Clone)] +pub enum NavBarEvent { + SectionSelected(NavBarSection), + PageSelected(NavBarSection, NavBarPage), +} + +#[derive(Default)] +pub struct NavBarState { + selected_section: NavBarSection, + section_active: bool, + selected_page: Option, + page_active: bool, +} + +impl<'a, Message> Component for NavBar<'a, Message> { + type State = NavBarState; + type Event = NavBarEvent; + + fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option { + match event { + NavBarEvent::SectionSelected(section) => { + if state.selected_section == section { + state.section_active = !state.section_active; + } else { + state.selected_section = section; + state.section_active = true; + } + state.selected_page = None; + state.page_active = false; + None + } + NavBarEvent::PageSelected(section, page) => { + if state.selected_page.is_some() && &page == state.selected_page.as_ref().unwrap() { + state.page_active = !state.page_active; + } else { + state.selected_page = Some(page.clone()); + state.page_active = true; + } + self.on_page_selected + .as_ref() + .map(|on_page_selected| (on_page_selected)(section, page)) + } + } + } + + fn view(&self, state: &Self::State) -> Element<'a, Self::Event, Renderer> { + if self.active { + let mut sections: Vec> = vec![]; + let mut pages: Vec> = vec![]; + + for (section, section_pages) in &self.source { + sections.push( + button( + column(vec![ + icon(section.icon.clone(), 20).into(), + text(section.title.clone()).size(14).into(), + ]) + .width(Length::Units(100)) + .height(Length::Units(50)) + .align_items(Alignment::Center), + ) + .style( + if *section == state.selected_section && state.section_active { + theme::Button::Primary.into() + } else { + theme::Button::Text.into() + }, + ) + .on_press(NavBarEvent::SectionSelected(section.clone())) + .into(), + ); + if *section == state.selected_section { + for page in section_pages { + pages.push( + button(row![text(&page.title).size(16).width(Length::Fill)]) + .padding(10) + .style(if let Some(selected_page) = &state.selected_page { + if state.page_active && page == selected_page { + theme::Button::Primary.into() + } else { + theme::Button::Text.into() + } + } else { + theme::Button::Text.into() + }) + .on_press(NavBarEvent::PageSelected(section.clone(), page.clone())) + .into(), + ); + } + } + } + + let nav_bar: Element = + container(if self.condensed && state.selected_page.is_some() { + row![container(scrollable(column(pages) + .spacing(10) + .padding(10) + .max_width(200) + .width(Length::Units(200)) + .height(Length::Shrink))) + .height(Length::Fill) + .style(theme::Container::Custom(nav_bar_pages_style))] + } else if !state.section_active || self.condensed && state.selected_page.is_none() { + row![scrollable(column(sections) + .spacing(10) + .padding(10) + .max_width(100) + .align_items(Alignment::Center) + .height(Length::Shrink))] + } else { + row![ + scrollable(column(sections) + .spacing(10) + .padding(10) + .max_width(100) + .align_items(Alignment::Center) + .height(Length::Shrink)), + container(scrollable(column(pages) + .spacing(10) + .padding(10) + .max_width(200) + .width(Length::Units(200)) + .height(Length::Shrink))) + .height(Length::Fill) + .style(theme::Container::Custom(nav_bar_pages_style)), + ] + }) + .height(Length::Fill) + .style(theme::Container::Custom(nav_bar_sections_style)) + .into(); + nav_bar + } else { + row![].into() + } + } +} + +impl<'a, Message: 'static> From> + for Element<'a, Message, Renderer> +{ + fn from(nav_bar: NavBar<'a, Message>) -> Self { + iced_lazy::component(nav_bar) + } +} + +pub fn section_button_style(theme: &Theme) -> Appearance { + let primary = &theme.cosmic().primary; + Appearance { + shadow_offset: Default::default(), + background: Some(Background::Color(primary.base.into())), + border_radius: 5.0, + border_width: 0.0, + border_color: Default::default(), + text_color: Default::default(), + } +} diff --git a/src/widget/popup.rs b/src/widget/popup.rs new file mode 100644 index 00000000..780edc02 --- /dev/null +++ b/src/widget/popup.rs @@ -0,0 +1,311 @@ +use iced::futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; +use iced::futures::SinkExt; +use iced::{ + futures::StreamExt, + widget::{container, Container}, + Rectangle, +}; +use iced_native::alignment::{self, Alignment}; +use iced_native::command::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::widget::{Operation, Tree}; +use iced_native::{ + window, Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Shell, Widget, +}; +use std::u32; + +pub use iced_style::container::{Appearance, StyleSheet}; +pub struct SizeTrackingContainer<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, + Renderer::Theme: StyleSheet, +{ + container: Container<'a, Message, Renderer>, + tx: UnboundedSender>, +} + +impl<'a, Message, Renderer> SizeTrackingContainer<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates an empty [`Container`]. + pub fn new(content: T, tx: UnboundedSender>) -> Self + where + T: Into>, + { + SizeTrackingContainer { + container: container(content), + tx, + } + } + + /// Sets the [`Padding`] of the [`Container`]. + pub fn padding>(mut self, padding: P) -> Self { + self.container = self.container.padding(padding); + self + } + + /// Sets the width of the [`Container`]. + pub fn width(mut self, width: Length) -> Self { + self.container = self.container.width(width); + self + } + + /// Sets the height of the [`Container`]. + pub fn height(mut self, height: Length) -> Self { + self.container = self.container.height(height); + self + } + + /// Sets the maximum width of the [`Container`]. + pub fn max_width(mut self, max_width: u32) -> Self { + self.container = self.container.max_width(max_width); + self + } + + /// Sets the maximum height of the [`Container`] in pixels. + pub fn max_height(mut self, max_height: u32) -> Self { + self.container = self.container.max_height(max_height); + self + } + + /// Sets the content alignment for the horizontal axis of the [`Container`]. + pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { + self.container = self.container.align_x(alignment); + self + } + + /// Sets the content alignment for the vertical axis of the [`Container`]. + pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { + self.container = self.container.align_y(alignment); + self + } + + /// Centers the contents in the horizontal axis of the [`Container`]. + pub fn center_x(mut self) -> Self { + self.container = self.container.center_x(); + self + } + + /// Centers the contents in the vertical axis of the [`Container`]. + pub fn center_y(mut self) -> Self { + self.container = self.container.center_y(); + self + } + + /// Sets the style of the [`Container`]. + pub fn style(mut self, style: impl Into<::Style>) -> Self { + self.container = self.container.style(style); + self + } +} + +impl<'a, Message, Renderer> Widget + for SizeTrackingContainer<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, + Renderer::Theme: container::StyleSheet, +{ + fn children(&self) -> Vec { + self.container.children() + } + + fn diff(&self, tree: &mut Tree) { + self.container.diff(tree) + } + + fn width(&self) -> Length { + Widget::width(&self.container) + } + + fn height(&self) -> Length { + Widget::height(&self.container) + } + + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + self.container.layout(renderer, limits) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.container.on_event( + &mut tree.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.container.mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + let Rectangle { + x, + y, + width, + height, + } = layout.bounds(); + let _ = self.tx.unbounded_send(Rectangle { + x: x as i32, + y: y as i32, + width: width as i32, + height: height as i32, + }); + self.container.draw( + &tree.children[0], + renderer, + theme, + inherited_style, + layout, + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.container + .overlay(&mut tree.children[0], layout, renderer) + } +} + +pub struct PopupParentSubscription { + id: window::Id, + settings: SctkPopupSettings, +} + +impl PopupParentSubscription { + pub fn new(id: window::Id, settings: SctkPopupSettings) -> Self { + Self { id, settings } + } + + pub fn get_popup_container<'a, T, Message, Renderer>( + &self, + content: T, + tx: UnboundedSender>, + ) -> SizeTrackingContainer<'a, Message, Renderer> + where + T: Into>, + Renderer: iced_native::Renderer, + Renderer::Theme: StyleSheet, + { + SizeTrackingContainer::new(content, tx.clone()) + } + + pub fn subscription(&self) -> iced::Subscription<(window::Id, PositionerUpdate)> { + popup_resize(self.id, self.settings.clone()) + } +} + +pub fn popup_resize( + id: window::Id, + settings: SctkPopupSettings, +) -> iced::Subscription<(window::Id, PositionerUpdate)> { + iced_native::subscription::unfold( + id, + State::Init(settings.positioner.anchor_rect.clone()), + move |state| rectangle_size(id, state), + ) + .with(settings) + .map(|(settings, (id, update))| match update { + RectangleUpdate::Update(rect) => { + let mut new_pos = settings.positioner.clone(); + new_pos.anchor_rect = rect; + (id, PositionerUpdate::Update(new_pos)) + } + RectangleUpdate::Finished => (id, PositionerUpdate::Finished), + RectangleUpdate::Sender(sender) => (id, PositionerUpdate::Sender(sender)), + }) +} + +#[derive(Debug, Clone)] +pub enum PositionerUpdate { + Sender(UnboundedSender>), + Update(SctkPositioner), + Finished, +} + +#[derive(Debug, Clone)] +pub enum RectangleUpdate { + Sender(UnboundedSender>), + Update(Rectangle), + Finished, +} + +pub enum State { + Init(Rectangle), + WaitForUpdate(Rectangle, UnboundedReceiver>), + Finished, +} + +async fn rectangle_size(id: I, state: State) -> (Option<(I, RectangleUpdate)>, State) { + match state { + State::Init(rectangle) => { + let (tx, rx) = unbounded(); + ( + Some((id, RectangleUpdate::Sender(tx))), + State::WaitForUpdate(rectangle, rx), + ) + } + State::WaitForUpdate(old_rectangle, mut rx) => { + let response = rx.next().await; + + match response { + Some(new_rectangle) => { + let new_update = if new_rectangle == old_rectangle { + None + } else { + Some((id, RectangleUpdate::Update(new_rectangle))) + }; + (new_update, State::WaitForUpdate(new_rectangle, rx)) + } + None => (Some((id, RectangleUpdate::Finished)), State::Finished), + } + } + State::Finished => iced::futures::future::pending().await, + } +} diff --git a/src/widget/scrollable.rs b/src/widget/scrollable.rs new file mode 100644 index 00000000..e205a2ea --- /dev/null +++ b/src/widget/scrollable.rs @@ -0,0 +1,11 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{Element, Renderer}; +use iced::widget; + +pub fn scrollable<'a, Message>(element: impl Into>) -> widget::Scrollable<'a, Message, Renderer> { + widget::scrollable(element) + .scrollbar_width(8) + .scroller_width(8) +} \ No newline at end of file diff --git a/src/widget/separator.rs b/src/widget/separator.rs new file mode 100644 index 00000000..f00bea74 --- /dev/null +++ b/src/widget/separator.rs @@ -0,0 +1,25 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::iced::widget; +use crate::{theme, Renderer, Theme}; + +#[must_use] +pub fn horizontal_rule(size: u16) -> widget::Rule { + widget::horizontal_rule(size).style(theme::Rule::Custom(separator_style)) +} + +#[must_use] +pub fn vertical_rule(size: u16) -> widget::Rule { + widget::vertical_rule(size).style(theme::Rule::Custom(separator_style)) +} + +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), + } +} \ No newline at end of file diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs new file mode 100644 index 00000000..d2447510 --- /dev/null +++ b/src/widget/settings/item.rs @@ -0,0 +1,26 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{Element, Renderer}; + +/// A setting within a settings view section. +#[must_use] +#[allow(clippy::module_name_repetitions)] +pub fn item<'a, Message: 'static>(title: &'a str, widget: impl Into>) -> iced::widget::Row<'a, Message, Renderer> { + item_row(vec![ + iced::widget::text(title).into(), + iced::widget::horizontal_space(iced::Length::Fill).into(), + widget.into() + ]) +} + +/// A settings item aligned in a row +#[must_use] +#[allow(clippy::module_name_repetitions)] +pub fn item_row(children: Vec>) -> iced::widget::Row { + iced::widget::row(children) + .align_items(iced::Alignment::Center) + .padding([0, 8]) + .spacing(12) +} + diff --git a/src/widget/settings/mod.rs b/src/widget/settings/mod.rs new file mode 100644 index 00000000..49ef389e --- /dev/null +++ b/src/widget/settings/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +mod item; +mod section; + +pub use self::item::{item, item_row}; +pub use self::section::{Section, view_section}; + +use crate::{Element, Renderer}; +use iced::widget::{Column, column}; + +/// A column with a predefined style for creating a settings panel +#[must_use] +pub fn view_column(children: Vec>) -> Column { + column(children).spacing(24).padding(24).max_width(600) +} diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs new file mode 100644 index 00000000..6f793522 --- /dev/null +++ b/src/widget/settings/section.rs @@ -0,0 +1,39 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{Element}; +use crate::widget::{ListColumn}; +use iced::widget::{column, text}; + +/// A section within a settings view column. +#[must_use] +pub fn view_section( + title: &str, +) -> Section { + Section { title, children: ListColumn::default() } +} + +pub struct Section<'a, Message> { + title: &'a str, + children: ListColumn<'a, Message> +} + +impl<'a, Message: 'static> Section<'a, Message> { + #[must_use] + pub fn add(mut self, item: impl Into>) -> Self { + self.children = self.children.add(item.into()); + self + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(data: Section<'a, Message>) -> Self { + let title = text(data.title) + .font(crate::font::FONT_SEMIBOLD) + .into(); + + column(vec![title, data.children.into_element()]) + .spacing(8) + .into() + } +} \ No newline at end of file diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs new file mode 100644 index 00000000..004f81da --- /dev/null +++ b/src/widget/toggler.rs @@ -0,0 +1,16 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::Renderer; +use iced::{widget, Length}; + +pub fn toggler<'a, Message>( + label: impl Into>, + is_checked: bool, + f: impl Fn(bool) -> Message + 'a, +) -> widget::Toggler<'a, Message, Renderer> { + widget::Toggler::new(is_checked, label, f) + .size(24) + .spacing(12) + .width(Length::Shrink) +} diff --git a/src/x.rs b/src/x.rs deleted file mode 100644 index 95784769..00000000 --- a/src/x.rs +++ /dev/null @@ -1,188 +0,0 @@ -use cascade::cascade; -use gdk4_x11::x11::xlib; -use glib::translate::ToGlibPtr; -use gtk4::{glib, prelude::*}; -use std::{ - ffi::{CString, NulError}, - os::raw::c_long, - ptr, -}; - -pub use std::os::raw::{c_int, c_uchar, c_ulong, c_ushort}; - -pub fn get_window_x11>( - window: &T, -) -> Option<(gdk4_x11::X11Display, gdk4_x11::X11Surface)> { - let surface = window - .upcast_ref() - .surface() - .downcast::() - .ok()?; - let display = surface.display().downcast::().ok()?; - Some((display, surface)) -} - -#[repr(transparent)] -pub struct Atom(xlib::Atom); - -impl Atom { - pub fn new(display: &gdk4_x11::X11Display, prop: &str) -> Result { - unsafe { - let prop = CString::new(prop)?; - Ok(Self(gdk4_x11::ffi::gdk_x11_get_xatom_by_name_for_display( - display.to_glib_none().0, - prop.as_ptr(), - ))) - } - } -} - -pub unsafe trait XElement { - const TYPE: xlib::Atom; - const SIZE: c_int; -} - -unsafe impl XElement for c_uchar { - const TYPE: xlib::Atom = xlib::XA_CARDINAL; - const SIZE: c_int = 8; -} - -unsafe impl XElement for c_ushort { - const TYPE: xlib::Atom = xlib::XA_CARDINAL; - const SIZE: c_int = 16; -} - -unsafe impl XElement for c_ulong { - const TYPE: xlib::Atom = xlib::XA_CARDINAL; - const SIZE: c_int = 32; -} - -unsafe impl XElement for Atom { - const TYPE: xlib::Atom = xlib::XA_ATOM; - const SIZE: c_int = 32; -} - -pub unsafe trait XProp { - const TYPE: xlib::Atom; - const SIZE: c_int; - - fn ptr(&self) -> *const u8; - - fn nelements(&self) -> c_int; -} - -unsafe impl XProp for &[T; LEN] { - const TYPE: xlib::Atom = T::TYPE; - const SIZE: c_int = T::SIZE; - - fn ptr(&self) -> *const u8 { - self.as_ptr() as _ - } - - fn nelements(&self) -> c_int { - LEN as c_int - } -} - -unsafe impl XProp for &[T] { - const TYPE: xlib::Atom = T::TYPE; - const SIZE: c_int = T::SIZE; - - fn ptr(&self) -> *const u8 { - self.as_ptr() as _ - } - - fn nelements(&self) -> c_int { - self.len() as c_int - } -} - -unsafe impl XProp for &str { - const TYPE: xlib::Atom = xlib::XA_STRING; - const SIZE: c_int = 8; - - fn ptr(&self) -> *const u8 { - self.as_ptr() - } - - fn nelements(&self) -> c_int { - self.len() as c_int - } -} - -#[allow(dead_code)] -pub enum PropMode { - Replace, - Prepend, - Append, -} - -pub unsafe fn change_property( - display: &gdk4_x11::X11Display, - surface: &gdk4_x11::X11Surface, - prop: &str, - mode: PropMode, - value: T, -) { - // TODO check error return value - let mode = match mode { - PropMode::Replace => xlib::PropModeReplace, - PropMode::Prepend => xlib::PropModePrepend, - PropMode::Append => xlib::PropModeAppend, - }; - xlib::XChangeProperty( - display.xdisplay(), - surface.xid(), - Atom::new(display, prop).unwrap().0, - T::TYPE, - T::SIZE, - mode, - value.ptr(), - value.nelements(), - ); -} - -pub unsafe fn set_position( - display: &gdk4_x11::X11Display, - surface: &gdk4_x11::X11Surface, - x: c_int, - y: c_int, -) { - // XXX check error return value - xlib::XMoveWindow(display.xdisplay(), surface.xid(), x, y); -} - -pub unsafe fn wm_state_add( - display: &gdk4_x11::X11Display, - surface: &gdk4_x11::X11Surface, - state: &str, -) { - const _NET_WM_STATE_ADD: c_long = 1; - // XXX check error return value - let mut event = xlib::XEvent { - client_message: xlib::XClientMessageEvent { - type_: xlib::ClientMessage, - serial: 0, - send_event: 0, - display: ptr::null_mut(), - window: surface.xid(), - message_type: Atom::new(display, "_NET_WM_STATE").unwrap().0, - format: 32, - data: cascade! { - xlib::ClientMessageData::new(); - ..set_long(0, _NET_WM_STATE_ADD); - ..set_long(1, Atom::new(display, state).unwrap().0 as _); - ..set_long(2, Atom::new(display, "").unwrap().0 as _); - ..set_long(3, 1); - ..set_long(3, 0); - }, - }, - }; - xlib::XSendEvent( - display.xdisplay(), - display.xrootwindow(), - 0, - xlib::SubstructureRedirectMask | xlib::SubstructureNotifyMask, - &mut event, - ); -} diff --git a/widgets/Cargo.toml b/widgets/Cargo.toml deleted file mode 100644 index c1229f5a..00000000 --- a/widgets/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "libcosmic-widgets" -version = "0.1.0" -edition = "2021" - -[dependencies] -relm4 = { git = "https://github.com/Relm4/Relm4", branch = "next" } -relm4-macros = { git = "https://github.com/Relm4/Relm4", branch = "next" } -tracker = "0.1.1" diff --git a/widgets/src/labeled_item/imp.rs b/widgets/src/labeled_item/imp.rs deleted file mode 100644 index 8c84bd59..00000000 --- a/widgets/src/labeled_item/imp.rs +++ /dev/null @@ -1,173 +0,0 @@ -use relm4::{ - component::ComponentSenderInner, - gtk::{prelude::*, Align, Box as GtkBox, Label, Orientation, Widget}, - ComponentParts, ComponentSender, SimpleComponent, -}; -use std::{cell::RefCell, sync::Arc}; - -#[derive(Debug)] -pub(crate) enum LabeledItemMessage { - Title(String), - Desc(Option), - Align(Align), - Child(Widget), -} - -#[track] -pub(crate) struct LabeledItem { - _title: String, - _desc: Option, - _align: Align, - _child: Option, - #[do_not_track] - _remove_child: RefCell>, - #[do_not_track] - _sender: ComponentSender, -} - -impl LabeledItem { - pub fn title(&self) -> &str { - &self._title - } - - pub fn description(&self) -> Option<&String> { - self._desc.as_ref() - } - - pub fn alignment(&self) -> Align { - self._align - } - - pub fn child(&self) -> Option<&Widget> { - self._child.as_ref() - } - - pub fn set_title(&self, title: S) - where - S: ToString, - { - self._sender - .input(LabeledItemMessage::Title(title.to_string())); - } - - pub fn set_description<'a, O>(&self, description: O) - where - O: Into>, - { - let description = description.into(); - self._sender - .input(LabeledItemMessage::Desc(description.map(|s| s.to_string()))); - } - - pub fn set_alignment(&self, align: Align) { - self._sender.input(LabeledItemMessage::Align(align)); - } - - pub fn set_child(&self, child: Widget) { - self._sender.input(LabeledItemMessage::Child(child)); - } -} - -#[component(pub(crate))] -impl SimpleComponent for LabeledItem { - type Widgets = AppWidgets; - type InitParams = (); - type Input = LabeledItemMessage; - type Output = (); - - view! { - base_box = GtkBox { - add_css_class: "labeled-item", - set_orientation: Orientation::Horizontal, - set_hexpand: true, - set_margin_start: 24, - set_margin_end: 24, - set_margin_top: 8, - set_margin_bottom: 8, - set_spacing: 16, - append: labeled_item_info = &GtkBox { - add_css_class: "labeled-item-info", - set_orientation: Orientation::Vertical, - set_hexpand: true, - set_spacing: 8, - set_valign: Align::Center, - Label { - add_css_class: "labeled-item-title", - set_halign: Align::Start, - #[watch] - set_label: &model._title - }, - Label { - add_css_class: "labeled-item-desc", - set_halign: Align::Start, - #[watch] - set_visible: model._desc.is_some(), - #[watch] - set_label: &model._desc.clone().unwrap_or_default() - }, - } - } - } - - fn init( - _init_params: Self::InitParams, - root: &Self::Root, - _sender: Arc>, - ) -> ComponentParts { - let model = LabeledItem { - _title: String::default(), - _desc: None, - _align: Align::Start, - _child: None, - _remove_child: RefCell::new(None), - _sender: _sender.clone(), - tracker: 0, - }; - let widgets = view_output!(); - - ComponentParts { model, widgets } - } - - fn update( - &mut self, - msg: Self::Input, - _sender: Arc>, - ) { - self.reset(); - match msg { - LabeledItemMessage::Title(title) => self.set__title(title), - LabeledItemMessage::Desc(desc) => self.set__desc(desc), - LabeledItemMessage::Align(align) => self.set__align(align), - LabeledItemMessage::Child(child) => { - *self._remove_child.borrow_mut() = self._child.take(); - self.set__child(Some(child)) - } - } - } - - fn post_view() { - if let Some(child) = self._remove_child.borrow_mut().take() { - widgets.base_box.remove(&child); - } - if self.changed(LabeledItem::_child()) { - let child = self._child.as_ref().expect("there's no child??"); - widgets.base_box.append(child); - } - if self.changed(LabeledItem::_align()) { - let child = self._child.as_ref().expect("set alignment without child"); - match self._align { - Align::Start => { - widgets - .base_box - .reorder_child_after(&widgets.labeled_item_info, Some(child)); - } - Align::End => { - widgets - .base_box - .reorder_child_after(child, Some(&widgets.labeled_item_info)); - } - _ => unimplemented!(), - } - } - } -} diff --git a/widgets/src/labeled_item/mod.rs b/widgets/src/labeled_item/mod.rs deleted file mode 100644 index 0684793d..00000000 --- a/widgets/src/labeled_item/mod.rs +++ /dev/null @@ -1,90 +0,0 @@ -mod imp; - -use relm4::{ - gtk::{glib::IsA, prelude::*, Align, Box as GtkBox, Orientation, Widget}, - Component, ComponentController, ComponentParts, Controller, -}; -use std::{cell::Ref, ops::Deref}; - -pub struct LabeledItem { - root: GtkBox, - controller: Controller, -} - -impl LabeledItem { - fn inner(&self) -> Ref<'_, ComponentParts> { - self.controller.state().get() - } - - pub fn new() -> Self { - Self::default() - } - - pub fn widget(&self) -> GtkBox { - self.root.clone() - } - - pub fn title(&self) -> String { - self.inner().model.title().to_owned() - } - - pub fn description(&self) -> Option { - self.inner().model.description().cloned() - } - - pub fn alignment(&self) -> Align { - self.inner().model.alignment() - } - - pub fn child(&self) -> Option { - self.inner().model.child().cloned() - } - - pub fn set_title(&self, title: S) - where - S: ToString, - { - self.inner().model.set_title(title) - } - - pub fn set_description<'a, O>(&self, description: O) - where - O: Into>, - { - self.inner().model.set_description(description) - } - - pub fn set_alignment(&self, align: Align) { - self.inner().model.set_alignment(align) - } - - pub fn set_child(&self, child: &impl IsA) { - let widget = child.upcast_ref(); - self.inner().model.set_child(widget.clone()); - } -} - -impl Default for LabeledItem { - fn default() -> Self { - let root = GtkBox::new(Orientation::Horizontal, 0); - let controller = imp::LabeledItem::builder() - .attach_to(&root) - .launch(()) - .detach(); - Self { root, controller } - } -} - -impl AsRef for LabeledItem { - fn as_ref(&self) -> &Widget { - self.root.upcast_ref() - } -} - -impl Deref for LabeledItem { - type Target = GtkBox; - - fn deref(&self) -> &Self::Target { - &self.root - } -} diff --git a/widgets/src/lib.rs b/widgets/src/lib.rs deleted file mode 100644 index dc90fc27..00000000 --- a/widgets/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[macro_use] -extern crate relm4_macros; -#[macro_use] -extern crate tracker; - -pub mod labeled_item; - -pub use labeled_item::LabeledItem; -pub use relm4;