/// Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 use cosmic::{ iced::widget::{column, container, horizontal_space, row, text}, iced::{self, Application, Command, Length, Subscription}, iced_native, iced_native::window, iced_winit::window::{close, drag, minimize, toggle_maximize}, theme::{self, Theme}, widget::{ header_bar, icon, list, nav_bar, nav_button, scrollable, segmented_button, settings, spin_button::{SpinButtonModel, SpinMessage}, }, Element, ElementExt, }; use std::{ sync::atomic::{AtomicU32, Ordering}, vec, }; mod bluetooth; mod demo; use self::{demo::DemoView, desktop::DesktopPage}; mod desktop; use self::input_devices::InputDevicesPage; mod input_devices; use self::networking::NetworkingPage; mod networking; use self::system_and_accounts::SystemAndAccountsPage; mod system_and_accounts; use self::time_and_language::TimeAndLanguagePage; mod time_and_language; pub trait SubPage { fn title(&self) -> &'static str; fn description(&self) -> &'static str; fn icon_name(&self) -> &'static str; fn parent_page(&self) -> Page; fn into_page(self) -> Page; } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Page { Demo, WiFi, Networking(Option), Bluetooth, Desktop(Option), InputDevices(Option), Displays, PowerAndBattery, Sound, PrintersAndScanners, PrivacyAndSecurity, SystemAndAccounts(Option), UpdatesAndRecovery, TimeAndLanguage(Option), Accessibility, Applications, } impl Page { //TODO: translate pub fn title(&self) -> &'static str { use Page::*; match self { Demo => "Demo", WiFi => "Wi-Fi", Networking(_) => "Networking", Bluetooth => "Bluetooth", Desktop(_) => "Desktop", InputDevices(_) => "Input Devices", Displays => "Displays", PowerAndBattery => "Power & Battery", Sound => "Sound", PrintersAndScanners => "Printers & Scanners", PrivacyAndSecurity => "Privacy & Security", SystemAndAccounts(_) => "System & Accounts", UpdatesAndRecovery => "Updates & Recovery", TimeAndLanguage(_) => "Time & Language", Accessibility => "Accessibility", Applications => "Applications", } } pub fn icon_name(&self) -> &'static str { use Page::*; match self { Demo => "document-properties-symbolic", WiFi => "network-wireless-symbolic", Networking(_) => "network-workgroup-symbolic", Bluetooth => "bluetooth-active-symbolic", Desktop(_) => "video-display-symbolic", InputDevices(_) => "input-keyboard-symbolic", Displays => "preferences-desktop-display-symbolic", PowerAndBattery => "battery-full-charged-symbolic", Sound => "multimedia-volume-control-symbolic", PrintersAndScanners => "printer-symbolic", PrivacyAndSecurity => "preferences-system-privacy-symbolic", SystemAndAccounts(_) => "system-users-symbolic", UpdatesAndRecovery => "software-update-available-symbolic", TimeAndLanguage(_) => "preferences-system-time-symbolic", Accessibility => "preferences-desktop-accessibility-symbolic", Applications => "preferences-desktop-apps-symbolic", } } } impl Default for Page { fn default() -> Page { //TODO: what should the default page be? Page::Desktop(None) } } static WINDOW_WIDTH: AtomicU32 = AtomicU32::new(0); const BREAK_POINT: u32 = 900; #[derive(Default)] pub struct Window { title: String, page: Page, debug: bool, theme: Theme, slider_value: f32, demo_tab_state: segmented_button::State, spin_button: SpinButtonModel, checkbox_value: bool, toggler_value: bool, pick_list_selected: Option<&'static str>, sidebar_toggled: bool, sidebar_toggled_condensed: bool, show_minimize: bool, show_maximize: 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 { ButtonPressed, CheckboxToggled(bool), Close, CondensedViewToggle(()), Debug(bool), DemoTabActivate(segmented_button::Key), Drag, InputChanged, Maximize, Minimize, Page(Page), PickListSelected(&'static str), RowSelected(usize), SliderChanged(f32), SpinButton(SpinMessage), ThemeChanged(Theme), TogglerToggled(bool), ToggleSidebar, ToggleSidebarCondensed, } impl Window { fn page_title(&self, page: Page) -> Element { row!(text(page.title()).size(30), horizontal_space(Length::Fill),).into() } fn is_condensed(&self) -> bool { WINDOW_WIDTH.load(Ordering::Relaxed) < BREAK_POINT } fn parent_page_button(&self, sub_page: impl SubPage) -> Element { let page = sub_page.parent_page(); column!( iced::widget::Button::new(row!( icon("go-previous-symbolic", 16).style(theme::Svg::SymbolicLink), text(page.title()).size(16), )) .padding(0) .style(theme::Button::Link) .on_press(Message::Page(page)), row!( text(sub_page.title()).size(30), horizontal_space(Length::Fill), ), ) .spacing(10) .into() } fn sub_page_button(&self, sub_page: impl SubPage) -> Element { iced::widget::Button::new( container( settings::item_row(vec![ icon(sub_page.icon_name(), 20) .style(theme::Svg::Symbolic) .into(), column!( text(sub_page.title()).size(18), text(sub_page.description()).size(12), ) .spacing(2) .into(), horizontal_space(iced::Length::Fill).into(), icon("go-next-symbolic", 20) .style(theme::Svg::Symbolic) .into(), ]) .spacing(16), ) .padding([20, 24]) .style(theme::Container::Custom(list::column::style)), ) .padding(0) .style(theme::Button::Transparent) .on_press(Message::Page(sub_page.into_page())) .into() } fn view_unimplemented_page(&self, page: Page) -> Element { settings::view_column(vec![ self.page_title(page), text("We haven't created that panel yet, and/or it is using a similar idea as current Pop! designs.").into(), ]).into() } fn view_unimplemented_sub_page(&self, sub_page: impl SubPage) -> Element { settings::view_column(vec![ self.parent_page_button(sub_page), text("We haven't created that panel yet, and/or it is using a similar idea as current Pop! designs.").into(), ]).into() } } 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.spin_button.min = -10; window.spin_button.max = 10; let key = window .demo_tab_state .insert(String::from("Tab A"), DemoView::TabA); window.demo_tab_state.activate(key); window .demo_tab_state .insert(String::from("Tab B"), DemoView::TabB); window .demo_tab_state .insert(String::from("Tab C"), DemoView::TabC); (window, Command::none()) } fn title(&self) -> String { self.title.clone() } fn subscription(&self) -> Subscription { iced_native::subscription::events_with(|event, _| match event { cosmic::iced::Event::Window( _window_id, window::Event::Resized { width, height: _ }, ) => { let old_width = WINDOW_WIDTH.load(Ordering::Relaxed); if old_width == 0 || old_width < BREAK_POINT && width > BREAK_POINT || old_width > BREAK_POINT && width < BREAK_POINT { WINDOW_WIDTH.store(width, Ordering::Relaxed); Some(()) } else { None } } _ => None, }) .map(Message::CondensedViewToggle) } fn update(&mut self, message: Message) -> iced::Command { match message { Message::Page(page) => { self.sidebar_toggled_condensed = false; 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::ToggleSidebar => self.sidebar_toggled = !self.sidebar_toggled, Message::ToggleSidebarCondensed => { self.sidebar_toggled_condensed = !self.sidebar_toggled_condensed } Message::Drag => return drag(window::Id::new(0)), Message::Close => return close(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 => {} Message::SpinButton(msg) => self.spin_button.update(msg), Message::CondensedViewToggle(_) => {} Message::DemoTabActivate(key) => self.demo_tab_state.activate(key), } Command::none() } fn view(&self) -> Element { let (sidebar_message, sidebar_toggled) = if self.is_condensed() { ( Message::ToggleSidebarCondensed, self.sidebar_toggled_condensed, ) } else { (Message::ToggleSidebar, self.sidebar_toggled) }; 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(sidebar_message) .sidebar_active(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); let mut widgets = Vec::with_capacity(2); if sidebar_toggled { let sidebar_button_complex = |page: Page, active| { cosmic::nav_button!(page.icon_name(), page.title(), active) .on_press(Message::Page(page)) }; let sidebar_button = |page: Page| sidebar_button_complex(page, self.page == page); let mut sidebar = container(scrollable( column!( sidebar_button(Page::Demo), sidebar_button(Page::WiFi), sidebar_button_complex( Page::Networking(None), matches!(self.page, Page::Networking(_)) ), sidebar_button(Page::Bluetooth), sidebar_button_complex( Page::Desktop(None), matches!(self.page, Page::Desktop(_)) ), sidebar_button_complex( Page::InputDevices(None), matches!(self.page, Page::InputDevices(_)) ), sidebar_button(Page::Displays), sidebar_button(Page::PowerAndBattery), sidebar_button(Page::Sound), sidebar_button(Page::PrintersAndScanners), sidebar_button(Page::PrivacyAndSecurity), sidebar_button_complex( Page::SystemAndAccounts(None), matches!(self.page, Page::SystemAndAccounts(_)) ), sidebar_button(Page::UpdatesAndRecovery), sidebar_button_complex( Page::TimeAndLanguage(None), matches!(self.page, Page::TimeAndLanguage(_)) ), sidebar_button(Page::Accessibility), sidebar_button(Page::Applications), ) .spacing(14), )) .height(Length::Fill) .padding(8) .style(theme::Container::Custom(nav_bar::nav_bar_sections_style)); if !self.is_condensed() { sidebar = sidebar.max_width(300) } let sidebar: Element<_> = sidebar.into(); widgets.push(sidebar.debug(self.debug)); } if !(self.is_condensed() && sidebar_toggled) { let content: Element<_> = match self.page { Page::Demo => self.view_demo(), Page::Networking(None) => settings::view_column(vec![ self.page_title(self.page), column!( self.sub_page_button(NetworkingPage::Wired), self.sub_page_button(NetworkingPage::OnlineAccounts), ) .spacing(16) .into(), ]) .into(), Page::Networking(Some(sub_page)) => self.view_unimplemented_sub_page(sub_page), Page::Bluetooth => self.view_bluetooth(), Page::Desktop(desktop_page_opt) => self.view_desktop(desktop_page_opt), Page::InputDevices(None) => settings::view_column(vec![ self.page_title(self.page), column!( self.sub_page_button(InputDevicesPage::Keyboard), self.sub_page_button(InputDevicesPage::Touchpad), self.sub_page_button(InputDevicesPage::Mouse), ) .spacing(16) .into(), ]) .into(), Page::InputDevices(Some(sub_page)) => self.view_unimplemented_sub_page(sub_page), Page::SystemAndAccounts(None) => settings::view_column(vec![ self.page_title(self.page), column!( self.sub_page_button(SystemAndAccountsPage::Users), self.sub_page_button(SystemAndAccountsPage::About), self.sub_page_button(SystemAndAccountsPage::Firmware), ) .spacing(16) .into(), ]) .into(), Page::SystemAndAccounts(Some(SystemAndAccountsPage::About)) => { self.view_system_and_accounts_about() } Page::SystemAndAccounts(Some(sub_page)) => { self.view_unimplemented_sub_page(sub_page) } Page::TimeAndLanguage(None) => settings::view_column(vec![ self.page_title(self.page), column!( self.sub_page_button(TimeAndLanguagePage::DateAndTime), self.sub_page_button(TimeAndLanguagePage::RegionAndLanguage), ) .spacing(16) .into(), ]) .into(), Page::TimeAndLanguage(Some(sub_page)) => self.view_unimplemented_sub_page(sub_page), _ => self.view_unimplemented_page(self.page), }; widgets.push( scrollable(row![ horizontal_space(Length::Fill), content.debug(self.debug), horizontal_space(Length::Fill), ]) .into(), ); } let content = container(row(widgets)) .padding([0, 8, 8, 8]) .width(Length::Fill) .height(Length::Fill) .into(); column(vec![header, content]).into() } fn theme(&self) -> Theme { self.theme } }