use cosmic::{ app::{Core, Task}, iced::runtime::core::window::Id as SurfaceId, iced::{ self, Rectangle, Size, Subscription, core::SmolStr, event::{ self, wayland::{Event as WaylandEvent, OutputEvent, SessionLockEvent}, }, keyboard::{Event as KeyEvent, Key, Modifiers}, }, widget, }; use cosmic_config::{ConfigSet, CosmicConfigEntry}; use cosmic_greeter_daemon::{BgSource, CosmicCompConfig, UserData}; use std::{collections::HashMap, sync::Arc}; use wayland_client::protocol::wl_output::WlOutput; pub const DEFAULT_MENU_ITEM_HEIGHT: f32 = 36.; #[derive(Debug, Clone, Eq, PartialEq)] pub struct ActiveLayout { pub layout: String, pub description: String, pub variant: String, } pub struct Common { pub active_layouts: Vec, pub active_surface_id_opt: Option, pub on_battery: bool, pub battery_percent: f64, pub charging_limit: Option, pub caps_lock: bool, pub comp_config_handler: Option, pub core: Core, pub error_opt: Option, pub fallback_background: widget::image::Handle, pub layouts_opt: Option>, pub network_icon_opt: Option, pub on_output_event: Option M>>, pub on_session_lock_event: Option M>>, pub output_names: HashMap, pub power_info_opt: Option<(widget::Icon, 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 { CapsLock(bool), Focus(SurfaceId), Key(Modifiers, Key, Option), NetworkIcon(Option<&'static str>), OutputEvent(OutputEvent, WlOutput), PowerInfo(Option<(f64, bool, bool)>), Prompt(String, bool, Option), 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 comp_config_handler = match cosmic_config::Config::new( "com.system76.CosmicComp", CosmicCompConfig::VERSION, ) { Ok(config_handler) => Some(config_handler), Err(err) => { tracing::error!("failed to create cosmic-comp config handler: {}", err); None } }; let layouts_opt = match xkb_data::all_keyboard_layouts() { Ok(ok) => Some(Arc::new(ok)), Err(err) => { tracing::warn!("failed to load keyboard layouts: {}", err); None } }; let app = Self { active_layouts: Vec::new(), active_surface_id_opt: None, caps_lock: false, comp_config_handler, core, error_opt: None, fallback_background: widget::image::Handle::from_bytes( include_bytes!("../res/background.jpg").as_slice(), ), layouts_opt, 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(), battery_percent: 0.0, on_battery: false, charging_limit: None, }; ( 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 set_xkb_config(&self, user_data: &UserData) { if let Some(mut xkb_config) = user_data.xkb_config_opt.clone() { xkb_config.layout = String::new(); xkb_config.variant = String::new(); for (i, layout) in self.active_layouts.iter().enumerate() { if i > 0 { xkb_config.layout.push(','); xkb_config.variant.push(','); } xkb_config.layout.push_str(&layout.layout); xkb_config.variant.push_str(&layout.variant); } if let Some(comp_config_handler) = &self.comp_config_handler { match comp_config_handler.set("xkb_config", xkb_config) { Ok(()) => tracing::info!("updated cosmic-comp xkb_config"), Err(err) => tracing::error!("failed to update cosmic-comp xkb_config: {}", err), } } } } 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; }; tracing::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 => { tracing::warn!( "output {}: failed to find wallpaper data for source {:?}", output_name, path ); } } break; } BgSource::Color(color) => { //TODO: support color sources tracing::warn!( "output {}: unsupported source {:?}", output_name, color ); } } } } } } pub fn update_user_data(&mut self, user_data: &UserData) { self.update_wallpapers(user_data); // From cosmic-applet-input-sources if let Some(keyboard_layouts) = &self.layouts_opt { if let Some(xkb_config) = &user_data.xkb_config_opt { self.active_layouts.clear(); let config_layouts = xkb_config.layout.split_terminator(','); let config_variants = xkb_config .variant .split_terminator(',') .chain(std::iter::repeat("")); 'outer: for (config_layout, config_variant) in config_layouts.zip(config_variants) { for xkb_layout in keyboard_layouts.layouts() { if config_layout != xkb_layout.name() { continue; } if config_variant.is_empty() { let active_layout = ActiveLayout { description: xkb_layout.description().to_owned(), layout: config_layout.to_owned(), variant: config_variant.to_owned(), }; self.active_layouts.push(active_layout); continue 'outer; } let Some(xkb_variants) = xkb_layout.variants() else { continue; }; for xkb_variant in xkb_variants { if config_variant != xkb_variant.name() { continue; } let active_layout = ActiveLayout { description: xkb_variant.description().to_owned(), layout: config_layout.to_owned(), variant: config_variant.to_owned(), }; self.active_layouts.push(active_layout); continue 'outer; } } } tracing::info!("{:?}", self.active_layouts); } } } pub fn update(&mut self, message: Message) -> Task { match message { Message::CapsLock(caps_lock) => { self.caps_lock = caps_lock; } 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::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 { if let Some((_, _, Some(value))) = &mut self.prompt_opt { value.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.map(|name| widget::icon::from_name(name).into()); } 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) => { if let Some((level, on_battery, threshold_enabled)) = power_info_opt { self.charging_limit = Some(threshold_enabled); self.update_battery(level, on_battery); } } Message::Prompt(prompt, secret, value_opt) => { let prompt_was_none = self.prompt_opt.is_none(); self.prompt_opt = Some((prompt, secret, value_opt)); if prompt_was_none { 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)) { tracing::info!("focus surface found id {:?}", text_input_id); return widget::text_input::focus(text_input_id.clone()); } } } } 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::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { Some(Message::CapsLock(modifiers.contains(Modifiers::CAPS_LOCK))) } 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) } } impl Common { fn update_battery(&mut self, mut percent: f64, on_battery: bool) { percent = percent.clamp(0.0, 100.0); self.on_battery = on_battery; self.battery_percent = percent; let battery_percent = if self.battery_percent > 95.0 && !self.charging_limit.unwrap_or_default() { 100 } else if self.battery_percent > 80.0 && !self.charging_limit.unwrap_or_default() { 90 } else if self.battery_percent > 65.0 { 80 } else if self.battery_percent > 35.0 { 50 } else if self.battery_percent > 20.0 { 35 } else if self.battery_percent > 14.0 { 20 } else if self.battery_percent > 9.0 { 10 } else if self.battery_percent > 5.0 { 5 } else { 0 }; let limited = if self.charging_limit.unwrap_or_default() { "limited-" } else { "" }; let charging = if on_battery { "" } else { "charging-" }; self.power_info_opt = Some(( widget::icon::from_name(format!( "cosmic-applet-battery-level-{battery_percent}-{limited}{charging}symbolic", )) .into(), percent, )); } }