diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..0aee09c --- /dev/null +++ b/src/common.rs @@ -0,0 +1,235 @@ +use cosmic::{ + app::{Core, Task}, + iced::{ + self, Rectangle, Size, Subscription, + core::SmolStr, + event::{ + self, + wayland::{Event as WaylandEvent, OutputEvent, SessionLockEvent}, + }, + keyboard::{Event as KeyEvent, Key, Modifiers}, + }, + iced_runtime::core::window::Id as SurfaceId, + widget, +}; +use cosmic_greeter_daemon::{BgSource, UserData}; +use std::collections::HashMap; +use wayland_client::protocol::wl_output::WlOutput; + +pub struct Common { + pub active_surface_id_opt: Option, + pub core: Core, + pub error_opt: Option, + pub input: String, + pub network_icon_opt: Option<&'static str>, + pub on_output_event: Option M>>, + pub on_session_lock_event: Option M>>, + pub output_names: HashMap, + pub power_info_opt: Option<(String, f64)>, + pub prompt_opt: Option<(String, bool, Option)>, + pub subsurface_rects: HashMap, + pub surface_ids: HashMap, + pub surface_images: HashMap, + pub surface_names: HashMap, + pub text_input_ids: HashMap, + pub time: crate::time::Time, + pub window_size: HashMap, +} + +#[derive(Clone, Debug)] +pub enum Message { + Focus(SurfaceId), + Input(String), + Key(Modifiers, Key, Option), + NetworkIcon(Option<&'static str>), + OutputEvent(OutputEvent, WlOutput), + PowerInfo(Option<(String, f64)>), + SessionLockEvent(SessionLockEvent), + Tick, + Tz(chrono_tz::Tz), +} + +impl + Send + 'static> Common { + pub fn init(mut core: Core) -> (Self, Task) { + core.window.show_window_menu = false; + core.window.show_headerbar = false; + // XXX must be false or define custom style to have transparent bg + core.window.sharp_corners = false; + core.window.show_maximize = false; + core.window.show_minimize = false; + core.window.use_template = false; + + let app = Self { + active_surface_id_opt: None, + core, + error_opt: None, + input: String::new(), + network_icon_opt: None, + on_output_event: None, + on_session_lock_event: None, + output_names: HashMap::new(), + power_info_opt: None, + prompt_opt: None, + subsurface_rects: HashMap::new(), + surface_ids: HashMap::new(), + surface_images: HashMap::new(), + surface_names: HashMap::new(), + text_input_ids: HashMap::new(), + time: crate::time::Time::new(), + window_size: HashMap::new(), + }; + ( + app, + Task::batch(vec![ + crate::time::tick().map(|_| cosmic::Action::App(Message::Tick.into())), + crate::time::tz_updates().map(|tz| cosmic::Action::App(Message::Tz(tz).into())), + ]), + ) + } + + pub fn update_wallpapers(&mut self, user_data: &UserData) { + for (_output, surface_id) in self.surface_ids.iter() { + if self.surface_images.contains_key(surface_id) { + continue; + } + + let Some(output_name) = self.surface_names.get(surface_id) else { + continue; + }; + + log::info!("updating wallpaper for {:?}", output_name); + + for (wallpaper_output_name, wallpaper_source) in user_data.bg_state.wallpapers.iter() { + if wallpaper_output_name == output_name { + match wallpaper_source { + BgSource::Path(path) => { + match user_data.bg_path_data.get(path) { + Some(bytes) => { + let image = widget::image::Handle::from_bytes(bytes.clone()); + self.surface_images.insert(*surface_id, image); + //TODO: what to do about duplicates? + } + None => { + log::warn!( + "output {}: failed to find wallpaper data for source {:?}", + output_name, + path + ); + } + } + break; + } + BgSource::Color(color) => { + //TODO: support color sources + log::warn!("output {}: unsupported source {:?}", output_name, color); + } + } + } + } + } + } + + pub fn update(&mut self, message: Message) -> Task { + match message { + Message::Focus(surface_id) => { + self.active_surface_id_opt = Some(surface_id); + if let Some(text_input_id) = self + .surface_names + .get(&surface_id) + .and_then(|id| self.text_input_ids.get(id)) + { + return widget::text_input::focus(text_input_id.clone()); + } + } + Message::Input(input) => { + self.input = input; + } + Message::Key(modifiers, key, text) => { + // Uncaptured keys with only shift modifiers go to the password box + if !modifiers.logo() + && !modifiers.control() + && !modifiers.alt() + && matches!(key, Key::Character(_)) + { + if let Some(text) = text { + self.input.push_str(&text); + } + + if let Some(surface_id) = self.active_surface_id_opt { + if let Some(text_input_id) = self + .surface_names + .get(&surface_id) + .and_then(|id| self.text_input_ids.get(id)) + { + return widget::text_input::focus(text_input_id.clone()); + } + } + } + } + Message::NetworkIcon(network_icon_opt) => { + self.network_icon_opt = network_icon_opt; + } + Message::OutputEvent(output_event, output) => { + if let Some(on_output_event) = &self.on_output_event { + return Task::done(cosmic::Action::App(on_output_event(output_event, output))); + } + } + Message::PowerInfo(power_info_opt) => { + self.power_info_opt = power_info_opt; + } + Message::SessionLockEvent(lock_event) => { + if let Some(on_session_lock_event) = &self.on_session_lock_event { + return Task::done(cosmic::Action::App(on_session_lock_event(lock_event))); + } + } + Message::Tick => { + self.time.tick(); + } + Message::Tz(tz) => { + self.time.set_tz(tz); + } + } + Task::none() + } + + pub fn subscription(&self) -> Subscription { + let mut subscriptions = Vec::with_capacity(3); + + subscriptions.push(event::listen_with(|event, status, id| match event { + iced::Event::Keyboard(KeyEvent::KeyPressed { + key, + modifiers, + text, + .. + }) => match status { + event::Status::Ignored => Some(Message::Key(modifiers, key, text)), + event::Status::Captured => None, + }, + iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland( + wayland_event, + )) => match wayland_event { + WaylandEvent::Output(output_event, output) => { + Some(Message::OutputEvent(output_event, output)) + } + WaylandEvent::SessionLock(lock_event) => { + Some(Message::SessionLockEvent(lock_event)) + } + _ => None, + }, + iced::Event::Window(iced::window::Event::Focused) => Some(Message::Focus(id)), + _ => None, + })); + + #[cfg(feature = "networkmanager")] + { + subscriptions.push(crate::networkmanager::subscription().map(Message::NetworkIcon)); + } + + #[cfg(feature = "upower")] + { + subscriptions.push(crate::upower::subscription().map(Message::PowerInfo)); + } + + Subscription::batch(subscriptions) + } +} diff --git a/src/greeter.rs b/src/greeter.rs index a58add4..9a1e2d0 100644 --- a/src/greeter.rs +++ b/src/greeter.rs @@ -6,7 +6,6 @@ mod ipc; use cosmic::app::{Core, Settings, Task}; use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity; use cosmic::iced::{Point, Size}; -use cosmic::iced_core::image; use cosmic::iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings; use cosmic::surface; use cosmic::widget::text; @@ -16,10 +15,7 @@ use cosmic::{ executor, iced::{ self, Background, Border, Length, Subscription, alignment, - event::{ - self, - wayland::{Event as WaylandEvent, OutputEvent}, - }, + event::wayland::OutputEvent, futures::SinkExt, platform_specific::{ runtime::wayland::layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings}, @@ -33,7 +29,7 @@ use cosmic::{ }; use cosmic_comp_config::CosmicCompConfig; use cosmic_greeter_config::Config as CosmicGreeterConfig; -use cosmic_greeter_daemon::{BgSource, UserData}; +use cosmic_greeter_daemon::UserData; use greetd_ipc::Request; use std::{ collections::{HashMap, hash_map}, @@ -49,7 +45,10 @@ use tokio::time; use wayland_client::{Proxy, protocol::wl_output::WlOutput}; use zbus::{Connection, proxy}; -use crate::fl; +use crate::{ + common::{self, Common}, + fl, +}; #[proxy( interface = "com.system76.CosmicGreeter", @@ -364,6 +363,8 @@ struct NameIndexPair { /// Messages that are used specifically by our [`App`]. #[derive(Clone, Debug)] pub enum Message { + Common(common::Message), + OutputEvent(OutputEvent, WlOutput), Auth(Option), ConfigUpdateUser, DialogCancel, @@ -371,15 +372,11 @@ pub enum Message { DropdownToggle(Dropdown), Error(String), Exit, - Focus(SurfaceId), // Sets channel used to communicate with the greetd IPC subscription. GreetdChannel(tokio::sync::mpsc::Sender), Heartbeat, KeyboardLayout(usize), Login, - NetworkIcon(Option<&'static str>), - OutputEvent(OutputEvent, WlOutput), - PowerInfo(Option<(String, f64)>), Prompt(String, bool, Option), Reconnect, Restart, @@ -388,41 +385,39 @@ pub enum Message { Socket(SocketState), Surface(surface::Action), Suspend, - Tick, - Tz(chrono_tz::Tz), Username(String), } +impl From for Message { + fn from(message: common::Message) -> Self { + Self::Common(message) + } +} + /// The [`App`] stores application-specific state. pub struct App { - core: Core, + common: Common, flags: Flags, greetd_sender: Option>, - surface_ids: HashMap, - active_surface_id_opt: Option, - surface_images: HashMap, - surface_names: HashMap, - text_input_ids: HashMap, - network_icon_opt: Option<&'static str>, - power_info_opt: Option<(String, f64)>, socket_state: SocketState, usernames: Vec<(String, String)>, selected_username: NameIndexPair, - prompt_opt: Option<(String, bool, Option)>, session_names: Vec, selected_session: String, active_layouts: Vec, - error_opt: Option, dialog_page_opt: Option, dropdown_opt: Option, - window_size: HashMap, heartbeat_handle: Option, - time: crate::time::Time, } impl App { fn menu(&self, id: SurfaceId) -> Element { - let window_width = self.window_size.get(&id).map(|s| s.width).unwrap_or(800.); + let window_width = self + .common + .window_size + .get(&id) + .map(|s| s.width) + .unwrap_or(800.); let menu_width = if window_width > 800. { 800. } else { @@ -435,15 +430,15 @@ impl App { .and_then(|i| self.flags.user_datas.get(i)) .map(|user_data| user_data.time_applet_config.military_time) .unwrap_or_default(); - let date_time_column = self.time.date_time_widget(military_time); + let date_time_column = self.common.time.date_time_widget(military_time); let mut status_row = widget::row::with_capacity(2).padding(16.0).spacing(12.0); - if let Some(network_icon) = self.network_icon_opt { + if let Some(network_icon) = self.common.network_icon_opt { status_row = status_row.push(widget::icon::from_name(network_icon)); } - if let Some((power_icon, power_percent)) = &self.power_info_opt { + if let Some((power_icon, power_percent)) = &self.common.power_info_opt { status_row = status_row.push(iced::widget::row![ widget::icon::from_name(power_icon.clone()), widget::text(format!("{:.0}%", power_percent)), @@ -641,18 +636,19 @@ impl App { ); } } - match &self.prompt_opt { + match &self.common.prompt_opt { Some((prompt, secret, value_opt)) => match value_opt { Some(value) => { let text_input_id = self + .common .surface_names .get(&id) - .and_then(|id| self.text_input_ids.get(id)) + .and_then(|id| self.common.text_input_ids.get(id)) .cloned() .unwrap_or_else(|| cosmic::widget::Id::new("text_input")); let mut text_input = widget::secure_input( prompt.clone(), - "", + &self.common.input, Some(Message::Prompt( prompt.clone(), !*secret, @@ -661,13 +657,14 @@ impl App { *secret, ) .id(text_input_id) - .manage_value(true) + .on_input(|input| common::Message::Input(input).into()) .on_submit(|v| Message::Auth(Some(v))); if let Some(text_input_id) = self + .common .surface_names .get(&id) - .and_then(|id| self.text_input_ids.get(id)) + .and_then(|id| self.common.text_input_ids.get(id)) { text_input = text_input.id(text_input_id.clone()); } @@ -698,7 +695,7 @@ impl App { } } - if let Some(error) = &self.error_opt { + if let Some(error) = &self.common.error_opt { column = column.push(widget::text(error)); } @@ -828,45 +825,7 @@ impl App { } }; - for (_output, surface_id) in self.surface_ids.iter() { - if self.surface_images.contains_key(surface_id) { - continue; - } - - let Some(output_name) = self.surface_names.get(surface_id) else { - continue; - }; - - log::info!("updating wallpaper for {:?}", output_name); - - for (wallpaper_output_name, wallpaper_source) in user_data.bg_state.wallpapers.iter() { - if wallpaper_output_name == output_name { - match wallpaper_source { - BgSource::Path(path) => { - match user_data.bg_path_data.get(path) { - Some(bytes) => { - let image = widget::image::Handle::from_bytes(bytes.clone()); - self.surface_images.insert(*surface_id, image); - //TODO: what to do about duplicates? - } - None => { - log::warn!( - "output {}: failed to find wallpaper data for source {:?}", - output_name, - path - ); - } - } - break; - } - BgSource::Color(color) => { - //TODO: support color sources - log::warn!("output {}: unsupported source {:?}", output_name, color); - } - } - } - } - } + self.common.update_wallpapers(&user_data); // From cosmic-applet-input-sources if let Some(keyboard_layouts) = &self.flags.layouts_opt { @@ -945,22 +904,19 @@ impl cosmic::Application for App { const APP_ID: &'static str = "com.system76.CosmicGreeter"; fn core(&self) -> &Core { - &self.core + &self.common.core } fn core_mut(&mut self) -> &mut Core { - &mut self.core + &mut self.common.core } /// Creates the application, and optionally emits command on initialize. - fn init(mut core: Core, flags: Self::Flags) -> (Self, Task) { - core.window.show_window_menu = false; - core.window.show_headerbar = false; - // XXX must be false or define custom style to have transparent bg - core.window.sharp_corners = false; - core.window.show_maximize = false; - core.window.show_minimize = false; - core.window.use_template = false; + fn init(core: Core, flags: Self::Flags) -> (Self, Task) { + let (mut common, common_task) = Common::init(core); + common.on_output_event = Some(Box::new(|output_event, output| { + Message::OutputEvent(output_event, output) + })); //TODO: use full_name? let mut usernames: Vec<_> = flags @@ -994,42 +950,28 @@ impl cosmic::Application for App { let selected_username = NameIndexPair { username, data_idx }; let app = App { - core, + common, flags, greetd_sender: None, - surface_ids: HashMap::new(), - active_surface_id_opt: None, - surface_images: HashMap::new(), - surface_names: HashMap::new(), - text_input_ids: HashMap::new(), - network_icon_opt: None, - power_info_opt: None, socket_state: SocketState::Pending, usernames, selected_username, - prompt_opt: None, session_names, selected_session, active_layouts: Vec::new(), - error_opt: None, dialog_page_opt: None, dropdown_opt: None, - window_size: HashMap::new(), heartbeat_handle: None, - time: crate::time::Time::new(), }; - ( - app, - Task::batch(vec![ - crate::time::tick().map(|_| cosmic::Action::App(Message::Tick)), - crate::time::tz_updates().map(|tz| cosmic::Action::App(Message::Tz(tz))), - ]), - ) + (app, common_task) } /// Handle application events here. fn update(&mut self, message: Self::Message) -> Task { match message { + Message::Common(common_message) => { + return self.common.update(common_message); + } Message::OutputEvent(output_event, output) => { match output_event { OutputEvent::Created(output_info_opt) => { @@ -1038,7 +980,7 @@ impl cosmic::Application for App { let surface_id = SurfaceId::unique(); let subsurface_id = SurfaceId::unique(); - match self.surface_ids.insert(output.clone(), surface_id) { + match self.common.surface_ids.insert(output.clone(), surface_id) { Some(old_surface_id) => { //TODO: remove old surface? log::warn!( @@ -1059,13 +1001,17 @@ impl cosmic::Application for App { match output_info_opt { Some(output_info) => match output_info.name { Some(output_name) => { - self.surface_names.insert(surface_id, output_name.clone()); - self.surface_names + self.common + .surface_names + .insert(surface_id, output_name.clone()); + self.common + .surface_names .insert(subsurface_id, output_name.clone()); - self.surface_images.remove(&surface_id); + self.common.surface_images.remove(&surface_id); let text_input_id = widget::Id::new(format!("input-{output_name}",)); - self.text_input_ids + self.common + .text_input_ids .insert(output_name.clone(), text_input_id.clone()); } None => { @@ -1091,7 +1037,7 @@ impl cosmic::Application for App { Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32 - 32.), ) }; - self.window_size.insert( + self.common.window_size.insert( surface_id, Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32), ); @@ -1139,12 +1085,12 @@ impl cosmic::Application for App { } OutputEvent::Removed => { log::info!("output {}: removed", output.id()); - match self.surface_ids.remove(&output) { + match self.common.surface_ids.remove(&output) { Some(surface_id) => { - self.surface_images.remove(&surface_id); - self.window_size.remove(&surface_id); - if let Some(n) = self.surface_names.remove(&surface_id) { - self.text_input_ids.remove(&n); + self.common.surface_images.remove(&surface_id); + self.common.window_size.remove(&surface_id); + if let Some(n) = self.common.surface_names.remove(&surface_id) { + self.common.text_input_ids.remove(&n); } return destroy_layer_surface(surface_id); } @@ -1170,25 +1116,21 @@ impl cosmic::Application for App { _ => {} } } - Message::NetworkIcon(network_icon_opt) => { - self.network_icon_opt = network_icon_opt; - } - Message::PowerInfo(power_info_opt) => { - self.power_info_opt = power_info_opt; - } Message::Prompt(prompt, secret, value_opt) => { let value_was_some = self + .common .prompt_opt .as_ref() .map_or(false, |(_, _, x)| x.is_some()); let value_is_some = value_opt.is_some(); - self.prompt_opt = Some((prompt, secret, value_opt)); + self.common.prompt_opt = Some((prompt, secret, value_opt)); if value_is_some && !value_was_some { - if let Some(surface_id) = self.active_surface_id_opt { + if let Some(surface_id) = self.common.active_surface_id_opt { if let Some(text_input_id) = self + .common .surface_names .get(&surface_id) - .and_then(|id| self.text_input_ids.get(id)) + .and_then(|id| self.common.text_input_ids.get(id)) { return widget::text_input::focus(text_input_id.clone()); } @@ -1208,7 +1150,7 @@ impl cosmic::Application for App { if username != self.selected_username.username { let data_idx = Self::user_data_index(&self.flags.user_datas, &username); self.selected_username = NameIndexPair { username, data_idx }; - self.surface_images.clear(); + self.common.surface_images.clear(); if let Some(session) = data_idx.and_then(|i| { self.flags .user_datas @@ -1227,7 +1169,7 @@ impl cosmic::Application for App { }; match &self.socket_state { SocketState::Open => { - self.prompt_opt = None; + self.common.prompt_opt = None; self.send_request(Request::CancelSession); } _ => {} @@ -1299,13 +1241,14 @@ impl cosmic::Application for App { } } Message::Auth(response) => { - self.prompt_opt = None; - self.error_opt = None; + self.common.input.clear(); + self.common.prompt_opt = None; + self.common.error_opt = None; self.send_request(Request::PostAuthMessageResponse { response }); } Message::Login => { - self.prompt_opt = None; - self.error_opt = None; + self.common.prompt_opt = None; + self.common.error_opt = None; match self.flags.sessions.get(&self.selected_session).cloned() { Some((cmd, env)) => { self.send_request(Request::StartSession { cmd, env }); @@ -1315,7 +1258,7 @@ impl cosmic::Application for App { } } Message::Error(error) => { - self.error_opt = Some(error); + self.common.error_opt = Some(error); self.send_request(Request::CancelSession); } Message::Reconnect => { @@ -1423,11 +1366,11 @@ impl cosmic::Application for App { }, Message::Exit => { let mut commands = Vec::new(); - for (_output, surface_id) in self.surface_ids.drain() { - self.surface_images.remove(&surface_id); - self.surface_names.remove(&surface_id); - if let Some(n) = self.surface_names.remove(&surface_id) { - self.text_input_ids.remove(&n); + for (_output, surface_id) in self.common.surface_ids.drain() { + self.common.surface_images.remove(&surface_id); + self.common.surface_names.remove(&surface_id); + if let Some(n) = self.common.surface_names.remove(&surface_id) { + self.common.text_input_ids.remove(&n); } commands.push(destroy_layer_surface(surface_id)); } @@ -1442,22 +1385,6 @@ impl cosmic::Application for App { cosmic::app::Action::Surface(a), )); } - Message::Focus(surface_id) => { - self.active_surface_id_opt = Some(surface_id); - if let Some(text_input_id) = self - .surface_names - .get(&surface_id) - .and_then(|id| self.text_input_ids.get(id)) - { - return widget::text_input::focus(text_input_id.clone()); - } - } - Message::Tick => { - self.time.tick(); - } - Message::Tz(tz) => { - self.time.set_tz(tz); - } } Task::none() } @@ -1470,6 +1397,7 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view_window(&self, surface_id: SurfaceId) -> Element { let img = self + .common .surface_images .get(&surface_id) .unwrap_or(&self.flags.fallback_background); @@ -1481,33 +1409,9 @@ impl cosmic::Application for App { } fn subscription(&self) -> Subscription { - let mut subscriptions = vec![ - event::listen_with(|event, _, id| match event { - iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland( - wayland_event, - )) => match wayland_event { - WaylandEvent::Output(output_event, output) => { - Some(Message::OutputEvent(output_event, output)) - } - - _ => None, - }, - iced::Event::Window(iced::window::Event::Focused) => Some(Message::Focus(id)), - _ => None, - }), + Subscription::batch([ + self.common.subscription().map(Message::from), ipc::subscription(), - ]; - - #[cfg(feature = "networkmanager")] - { - subscriptions.push(crate::networkmanager::subscription().map(Message::NetworkIcon)); - } - - #[cfg(feature = "upower")] - { - subscriptions.push(crate::upower::subscription().map(Message::PowerInfo)); - } - - Subscription::batch(subscriptions) + ]) } } diff --git a/src/lib.rs b/src/lib.rs index 31d008e..d709b49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ pub mod greeter; pub mod locker; +mod common; + mod localize; #[cfg(feature = "logind")] diff --git a/src/locker.rs b/src/locker.rs index 96fc4e4..f88a757 100644 --- a/src/locker.rs +++ b/src/locker.rs @@ -10,10 +10,7 @@ use cosmic::{ Element, executor, iced::{ self, Length, Subscription, alignment, - event::{ - self, - wayland::{Event as WaylandEvent, OutputEvent, SessionLockEvent}, - }, + event::wayland::{OutputEvent, SessionLockEvent}, futures::{self, SinkExt}, platform_specific::shell::wayland::commands::session_lock::{ destroy_lock_surface, get_lock_surface, lock, unlock, @@ -23,11 +20,10 @@ use cosmic::{ widget, }; use cosmic_config::CosmicConfigEntry; -use cosmic_greeter_daemon::{BgSource, TimeAppletConfig, UserData}; +use cosmic_greeter_daemon::{TimeAppletConfig, UserData}; use std::time::Duration; use std::{ any::TypeId, - collections::HashMap, env, ffi::{CStr, CString}, fs, @@ -39,6 +35,8 @@ use std::{ use tokio::{sync::mpsc, task}; use wayland_client::{Proxy, protocol::wl_output::WlOutput}; +use crate::common::{self, Common}; + fn lockfile_opt() -> Option { let runtime_dir = dirs::runtime_dir()?; let session_id = env::var("XDG_SESSION_ID").ok()?; @@ -191,26 +189,28 @@ pub struct Flags { #[derive(Clone, Debug)] pub enum Message { None, + Common(common::Message), OutputEvent(OutputEvent, WlOutput), SessionLockEvent(SessionLockEvent), Channel(mpsc::Sender), BackgroundState(cosmic_bg_config::state::State), TimeAppletConfig(TimeAppletConfig), - Focus(SurfaceId), Inhibit(Arc), - NetworkIcon(Option<&'static str>), - PowerInfo(Option<(String, f64)>), Prompt(String, bool, Option), Submit(String), Surface(surface::Action), Suspend, Error(String), Lock, - Tick, - Tz(chrono_tz::Tz), Unlock, } +impl From for Message { + fn from(message: common::Message) -> Self { + Self::Common(message) + } +} + #[derive(Clone, Debug)] enum State { Locking, @@ -233,29 +233,17 @@ impl Drop for State { /// The [`App`] stores application-specific state. pub struct App { - core: Core, + common: Common, flags: Flags, state: State, - output_names: HashMap, - surface_ids: HashMap, - subsurface_rects: HashMap, - active_surface_id_opt: Option, - surface_images: HashMap, - surface_names: HashMap, - text_input_ids: HashMap, inhibit_opt: Option>, - network_icon_opt: Option<&'static str>, - power_info_opt: Option<(String, f64)>, value_tx_opt: Option>, - prompt_opt: Option<(String, bool, Option)>, - error_opt: Option, - time: crate::time::Time, - window_size: HashMap, } impl App { fn menu(&self, surface_id: SurfaceId) -> Element { let window_width = self + .common .window_size .get(&surface_id) .map(|s| s.width) @@ -267,15 +255,15 @@ impl App { }; let left_element = { let military_time = self.flags.user_data.time_applet_config.military_time; - let date_time_column = self.time.date_time_widget(military_time); + let date_time_column = self.common.time.date_time_widget(military_time); let mut status_row = widget::row::with_capacity(2).padding(16.0).spacing(12.0); - if let Some(network_icon) = self.network_icon_opt { + if let Some(network_icon) = self.common.network_icon_opt { status_row = status_row.push(widget::icon::from_name(network_icon)); } - if let Some((power_icon, power_percent)) = &self.power_info_opt { + if let Some((power_icon, power_percent)) = &self.common.power_info_opt { status_row = status_row.push(iced::widget::row![ widget::icon::from_name(power_icon.clone()), widget::text(format!("{:.0}%", power_percent)), @@ -339,19 +327,20 @@ impl App { .align_x(alignment::Horizontal::Center), ); - match &self.prompt_opt { + match &self.common.prompt_opt { Some((prompt, secret, value_opt)) => match value_opt { Some(value) => { let text_input_id = self + .common .surface_names .get(&surface_id) - .and_then(|id| self.text_input_ids.get(id)) + .and_then(|id| self.common.text_input_ids.get(id)) .cloned() .unwrap_or_else(|| cosmic::widget::Id::new("text_input")); let mut text_input = widget::secure_input( prompt.clone(), - "", + &self.common.input, Some(Message::Prompt( prompt.clone(), !*secret, @@ -360,7 +349,7 @@ impl App { *secret, ) .id(text_input_id) - .manage_value(true) + .on_input(|input| common::Message::Input(input).into()) .on_submit(Message::Submit); if *secret { @@ -376,7 +365,7 @@ impl App { None => {} } - if let Some(error) = &self.error_opt { + if let Some(error) = &self.common.error_opt { column = column.push(widget::text(error)); } @@ -414,51 +403,6 @@ impl App { .class(cosmic::theme::Container::Transparent) .into() } - - //TODO: cache wallpapers by source? - fn update_wallpapers(&mut self) { - let user_data = &self.flags.user_data; - - for (_output, surface_id) in self.surface_ids.iter() { - if self.surface_images.contains_key(surface_id) { - continue; - } - - let Some(output_name) = self.surface_names.get(surface_id) else { - continue; - }; - - log::info!("updating wallpaper for {:?}", output_name); - - for (wallpaper_output_name, wallpaper_source) in user_data.bg_state.wallpapers.iter() { - if wallpaper_output_name == output_name { - match wallpaper_source { - BgSource::Path(path) => { - match user_data.bg_path_data.get(path) { - Some(bytes) => { - let image = widget::image::Handle::from_bytes(bytes.clone()); - self.surface_images.insert(*surface_id, image); - //TODO: what to do about duplicates? - } - None => { - log::warn!( - "output {}: failed to find wallpaper data for source {:?}", - output_name, - path - ); - } - } - break; - } - BgSource::Color(color) => { - //TODO: support color sources - log::warn!("output {}: unsupported source {:?}", output_name, color); - } - } - } - } - } - } } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -476,22 +420,20 @@ impl cosmic::Application for App { const APP_ID: &'static str = "com.system76.CosmicGreeter"; fn core(&self) -> &Core { - &self.core + &self.common.core } fn core_mut(&mut self) -> &mut Core { - &mut self.core + &mut self.common.core } /// Creates the application, and optionally emits command on initialize. - fn init(mut core: Core, flags: Self::Flags) -> (Self, Task) { - core.window.show_window_menu = false; - core.window.show_headerbar = false; - // XXX must be false or define custom style to have transparent bg - core.window.sharp_corners = false; - core.window.show_maximize = false; - core.window.show_minimize = false; - core.window.use_template = false; + fn init(core: Core, flags: Self::Flags) -> (Self, Task) { + let (mut common, common_task) = Common::init(core); + common.on_output_event = Some(Box::new(|output_event, output| { + Message::OutputEvent(output_event, output) + })); + common.on_session_lock_event = Some(Box::new(|evt| Message::SessionLockEvent(evt))); let already_locked = match flags.lockfile_opt { Some(ref lockfile) => lockfile.exists(), @@ -499,24 +441,11 @@ impl cosmic::Application for App { }; let mut app = App { - core, + common, flags, state: State::Unlocked, - surface_ids: HashMap::new(), - active_surface_id_opt: None, - output_names: HashMap::new(), - surface_images: HashMap::new(), - surface_names: HashMap::new(), - text_input_ids: HashMap::new(), - subsurface_rects: HashMap::new(), inhibit_opt: None, - network_icon_opt: None, - power_info_opt: None, value_tx_opt: None, - prompt_opt: None, - error_opt: None, - time: crate::time::Time::new(), - window_size: HashMap::new(), }; let task = if cfg!(feature = "logind") { @@ -536,20 +465,16 @@ impl cosmic::Application for App { lock() }; - ( - app, - Task::batch(vec![ - task, - crate::time::tick().map(|_| cosmic::Action::App(Message::Tick)), - crate::time::tz_updates().map(|tz| cosmic::Action::App(Message::Tz(tz))), - ]), - ) + (app, Task::batch([task, common_task])) } /// Handle application events here. fn update(&mut self, message: Self::Message) -> Task { match message { Message::None => {} + Message::Common(common_message) => { + return self.common.update(common_message); + } Message::OutputEvent(output_event, output) => { match output_event { OutputEvent::Created(output_info_opt) => { @@ -559,7 +484,7 @@ impl cosmic::Application for App { let subsurface_id = SurfaceId::unique(); if let Some(old_surface_id) = - self.surface_ids.insert(output.clone(), surface_id) + self.common.surface_ids.insert(output.clone(), surface_id) { //TODO: remove old surface? log::warn!( @@ -580,16 +505,21 @@ impl cosmic::Application for App { match output_info_opt { Some(output_info) => match output_info.name { Some(output_name) => { - self.output_names + self.common + .output_names .insert(output.clone(), output_name.clone()); - self.surface_names.insert(surface_id, output_name.clone()); - self.surface_names + self.common + .surface_names + .insert(surface_id, output_name.clone()); + self.common + .surface_names .insert(subsurface_id, output_name.clone()); - self.surface_images.remove(&surface_id); - self.update_wallpapers(); + self.common.surface_images.remove(&surface_id); + self.common.update_wallpapers(&self.flags.user_data); let text_input_id = widget::Id::new(format!("input-{output_name}",)); - self.text_input_ids + self.common + .text_input_ids .insert(output_name.clone(), text_input_id.clone()); } None => { @@ -615,12 +545,13 @@ impl cosmic::Application for App { Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32 - 32.), ) }; - self.window_size.insert( + self.common.window_size.insert( surface_id, Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32), ); - self.subsurface_rects + self.common + .subsurface_rects .insert(output.clone(), Rectangle::new(loc, sub_size)); let msg = cosmic::surface::action::subsurface( @@ -651,13 +582,13 @@ impl cosmic::Application for App { } OutputEvent::Removed => { log::info!("output {}: removed", output.id()); - match self.surface_ids.remove(&output) { + match self.common.surface_ids.remove(&output) { Some(surface_id) => { - self.surface_images.remove(&surface_id); - self.surface_names.remove(&surface_id); - self.window_size.remove(&surface_id); - if let Some(n) = self.surface_names.remove(&surface_id) { - self.text_input_ids.remove(&n); + self.common.surface_images.remove(&surface_id); + self.common.surface_names.remove(&surface_id); + self.common.window_size.remove(&surface_id); + if let Some(n) = self.common.surface_names.remove(&surface_id) { + self.common.text_input_ids.remove(&n); } if matches!(self.state, State::Locked { .. }) { return destroy_lock_surface(surface_id); @@ -685,7 +616,8 @@ impl cosmic::Application for App { } else { (Point::ORIGIN, Size::new(1920., 1080.)) }; - self.subsurface_rects + self.common + .subsurface_rects .insert(output.clone(), Rectangle::new(loc, sub_size)); log::info!("output {}: info update", output.id()); @@ -768,7 +700,7 @@ impl cosmic::Application for App { ) .abortable(); - let mut commands = Vec::with_capacity(self.surface_ids.len() + 1); + let mut commands = Vec::with_capacity(self.common.surface_ids.len() + 1); commands.push(locked_task); self.state = State::Locked { @@ -779,19 +711,22 @@ impl cosmic::Application for App { self.inhibit_opt = None; // Create lock surfaces - for (output, surface_id) in self.surface_ids.iter() { + for (output, surface_id) in self.common.surface_ids.iter() { commands.push(get_lock_surface(*surface_id, output.clone())); if let Some((rect, name)) = self + .common .subsurface_rects .get(output) .copied() - .zip(self.output_names.get(output)) + .zip(self.common.output_names.get(output)) { let subsurface_id = SurfaceId::unique(); let surface_id = *surface_id; - self.surface_names.insert(surface_id, name.clone()); - self.surface_names.insert(subsurface_id, name.clone()); + self.common.surface_names.insert(surface_id, name.clone()); + self.common + .surface_names + .insert(subsurface_id, name.clone()); let msg = cosmic::surface::action::subsurface( move |_: &mut App| SctkSubsurfaceSettings { parent: surface_id, @@ -822,9 +757,9 @@ impl cosmic::Application for App { self.state = State::Unlocked; let mut commands = Vec::new(); - for (_output, surface_id) in self.surface_ids.iter() { - self.surface_names.remove(surface_id); - self.window_size.remove(surface_id); + for (_output, surface_id) in self.common.surface_ids.iter() { + self.common.surface_names.remove(surface_id); + self.common.window_size.remove(surface_id); commands.push(destroy_lock_surface(*surface_id)); } if cfg!(feature = "logind") { @@ -843,11 +778,10 @@ impl cosmic::Application for App { self.value_tx_opt = Some(value_tx); } Message::BackgroundState(bg_state) => { - eprintln!("{:#?}", bg_state); self.flags.user_data.bg_state = bg_state; self.flags.user_data.load_wallpapers_as_user(); - self.surface_images.clear(); - self.update_wallpapers(); + self.common.surface_images.clear(); + self.common.update_wallpapers(&self.flags.user_data); } Message::TimeAppletConfig(config) => { self.flags.user_data.time_applet_config = config; @@ -855,51 +789,37 @@ impl cosmic::Application for App { Message::Inhibit(inhibit) => { self.inhibit_opt = Some(inhibit); } - Message::NetworkIcon(network_icon_opt) => { - self.network_icon_opt = network_icon_opt; - } - Message::PowerInfo(power_info_opt) => { - self.power_info_opt = power_info_opt; - } - Message::Focus(surface_id) => { - self.active_surface_id_opt = Some(surface_id); - self.active_surface_id_opt = Some(surface_id); - if let Some(text_input_id) = self - .surface_names - .get(&surface_id) - .and_then(|id| self.text_input_ids.get(id)) - { - return widget::text_input::focus(text_input_id.clone()); - } - } Message::Prompt(prompt, secret, value_opt) => { - let prompt_was_none = self.prompt_opt.is_none(); - self.prompt_opt = Some((prompt, secret, value_opt)); + let prompt_was_none = self.common.prompt_opt.is_none(); + self.common.prompt_opt = Some((prompt, secret, value_opt)); if prompt_was_none { - if let Some(surface_id) = self.active_surface_id_opt { + if let Some(surface_id) = self.common.active_surface_id_opt { if let Some(text_input_id) = self + .common .surface_names .get(&surface_id) - .and_then(|id| self.text_input_ids.get(id)) + .and_then(|id| self.common.text_input_ids.get(id)) { - log::error!("focus surface found id {:?}", text_input_id); - + log::info!("focus surface found id {:?}", text_input_id); return widget::text_input::focus(text_input_id.clone()); } } } } - Message::Submit(value) => match self.value_tx_opt.take() { - Some(value_tx) => { - // Clear errors - self.error_opt = None; - return cosmic::task::future(async move { - value_tx.send(value).await.unwrap(); - Message::Channel(value_tx) - }); + Message::Submit(value) => { + self.common.input.clear(); + match self.value_tx_opt.take() { + Some(value_tx) => { + // Clear errors + self.common.error_opt = None; + return cosmic::task::future(async move { + value_tx.send(value).await.unwrap(); + Message::Channel(value_tx) + }); + } + None => log::warn!("tried to submit when value_tx_opt not set"), } - None => log::warn!("tried to submit when value_tx_opt not set"), - }, + } Message::Suspend => { #[cfg(feature = "logind")] return cosmic::Task::future(async move { crate::logind::suspend().await.err() }) @@ -909,14 +829,14 @@ impl cosmic::Application for App { }); } Message::Error(error) => { - self.error_opt = Some(error); + self.common.error_opt = Some(error); } Message::Lock => match self.state { State::Unlocked => { log::info!("session locking"); self.state = State::Locking; // Clear errors - self.error_opt = None; + self.common.error_opt = None; // Clear value_tx self.value_tx_opt = None; // Try to create lockfile when locking @@ -941,7 +861,7 @@ impl cosmic::Application for App { log::info!("sessing unlocking"); self.state = State::Unlocking; // Clear errors - self.error_opt = None; + self.common.error_opt = None; // Clear value_tx self.value_tx_opt = None; // Try to delete lockfile when unlocking @@ -952,11 +872,11 @@ impl cosmic::Application for App { } // Destroy lock surfaces - let mut commands = Vec::with_capacity(self.surface_ids.len() + 1); + let mut commands = Vec::with_capacity(self.common.surface_ids.len() + 1); - for (_output, surface_id) in self.surface_ids.iter() { - self.surface_names.remove(surface_id); - self.window_size.remove(&surface_id); + for (_output, surface_id) in self.common.surface_ids.iter() { + self.common.surface_names.remove(surface_id); + self.common.window_size.remove(&surface_id); commands.push(destroy_lock_surface(*surface_id)); } @@ -979,12 +899,6 @@ impl cosmic::Application for App { cosmic::app::Action::Surface(a), )); } - Message::Tick => { - self.time.tick(); - } - Message::Tz(tz) => { - self.time.set_tz(tz); - } } Task::none() } @@ -997,6 +911,7 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view_window(&self, surface_id: SurfaceId) -> Element { let img = self + .common .surface_images .get(&surface_id) .unwrap_or(&self.flags.fallback_background); @@ -1010,19 +925,7 @@ impl cosmic::Application for App { fn subscription(&self) -> Subscription { let mut subscriptions = Vec::with_capacity(7); - subscriptions.push(event::listen_with(|event, _, id| match event { - iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland( - wayland_event, - )) => match wayland_event { - WaylandEvent::Output(output_event, output) => { - Some(Message::OutputEvent(output_event, output)) - } - WaylandEvent::SessionLock(evt) => Some(Message::SessionLockEvent(evt)), - _ => None, - }, - iced::Event::Window(iced::window::Event::Focused) => Some(Message::Focus(id)), - _ => None, - })); + subscriptions.push(self.common.subscription().map(Message::from)); struct BackgroundSubscription; subscriptions.push( @@ -1059,16 +962,6 @@ impl cosmic::Application for App { subscriptions.push(crate::logind::subscription()); } - #[cfg(feature = "networkmanager")] - { - subscriptions.push(crate::networkmanager::subscription().map(Message::NetworkIcon)); - } - - #[cfg(feature = "upower")] - { - subscriptions.push(crate::upower::subscription().map(Message::PowerInfo)); - } - Subscription::batch(subscriptions) } }