From d6b94406e6fa8933edfb34a2ce0280289d920a92 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 12 Mar 2025 16:07:57 -0400 Subject: [PATCH] refactor: lock surface subsurface menu --- Cargo.lock | 30 +- src/greeter.rs | 56 +-- src/image_container.rs | 160 ------- src/lib.rs | 1 - src/locker.rs | 942 +++++++++++++++++++++++------------------ 5 files changed, 573 insertions(+), 616 deletions(-) delete mode 100644 src/image_container.rs diff --git a/Cargo.lock b/Cargo.lock index adb8795..6cbc7fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1046,7 +1046,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "atomicwrites", "calloop 0.14.2", @@ -1069,7 +1069,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "quote", "syn 1.0.109", @@ -1212,7 +1212,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "almost", "cosmic-config", @@ -2487,7 +2487,7 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "dnd", "iced_accessibility", @@ -2505,7 +2505,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "accesskit", "accesskit_winit", @@ -2514,7 +2514,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "bitflags 2.8.0", "bytes", @@ -2538,7 +2538,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "futures", "iced_core", @@ -2564,7 +2564,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "bitflags 2.8.0", "bytemuck", @@ -2586,7 +2586,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2598,7 +2598,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "bytes", "cosmic-client-toolkit", @@ -2613,7 +2613,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "bytemuck", "cosmic-text", @@ -2629,7 +2629,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "as-raw-xcb-connection", "bitflags 2.8.0", @@ -2660,7 +2660,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -2678,7 +2678,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -3144,7 +3144,7 @@ checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#4c78a5620218a67f6653b171e9136f4bd9e89bf1" +source = "git+https://github.com/pop-os/libcosmic//?branch=drop-menu-tree-changes#5976b48f677649067d1240f6386d153261204278" dependencies = [ "apply", "ashpd 0.9.2", diff --git a/src/greeter.rs b/src/greeter.rs index c6c05db..e1a9709 100644 --- a/src/greeter.rs +++ b/src/greeter.rs @@ -5,7 +5,7 @@ mod ipc; use cosmic::app::{Core, Settings, Task}; use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity; -use cosmic::iced::{Point, Rectangle, Size}; +use cosmic::iced::{Point, Size}; use cosmic::iced_core::{image, window}; use cosmic::iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings; use cosmic::surface; @@ -400,11 +400,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), - LayerEvent(LayerEvent, SurfaceId), Login, NetworkIcon(Option<&'static str>), None, @@ -419,7 +419,6 @@ pub enum Message { Surface(surface::Action), Suspend, Username(String), - WindowOpen(SurfaceId), } /// The [`App`] stores application-specific state. @@ -718,9 +717,6 @@ impl App { ) .id(text_input_id) .manage_value(true) - // .on_input(|value| { - // Message::Prompt(prompt.clone(), *secret, Some(value)) - // }) .on_submit(|v| Message::Auth(Some(v))); if let Some(text_input_id) = self @@ -1141,13 +1137,16 @@ impl cosmic::Application for App { Size::new(800., unwrapped_size.1 as f32 - 32.), ) } else { - (Point::ORIGIN, Size::new(1920., 1080.)) + ( + Point::new(0., 32.), + Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32 - 32.), + ) }; self.window_size.insert( surface_id, Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32), ); - dbg!(loc, size); + let msg = cosmic::surface::action::subsurface( move |_: &mut App| SctkSubsurfaceSettings { parent: surface_id, @@ -1209,20 +1208,6 @@ impl cosmic::Application for App { } } } - Message::LayerEvent(layer_event, surface_id) => match layer_event { - LayerEvent::Focused => { - log::info!("focus surface {:?}", 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::Socket(socket_state) => { self.socket_state = socket_state; match &self.socket_state { @@ -1481,22 +1466,17 @@ impl cosmic::Application for App { cosmic::app::Action::Surface(a), )); } - Message::WindowOpen(id) if self.surface_ids.values().any(|i| *i == id) => { - if let Some(text_input_id) = self.surface_names.get(&id).and_then(|id| { - if self - .active_surface_id_opt - .and_then(|active_id| self.surface_names.get(&active_id)) - == Some(id) - { - self.text_input_ids.get(id) - } else { - None - } - }) { + + 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::WindowOpen(_) => {} } Task::none() } @@ -1530,12 +1510,10 @@ impl cosmic::Application for App { WaylandEvent::Output(output_event, output) => { Some(Message::OutputEvent(output_event, output)) } - WaylandEvent::Layer(layer_event, _surface, surface_id) => { - Some(Message::LayerEvent(layer_event, surface_id)) - } + _ => None, }, - iced::Event::Window(window::Event::Opened { .. }) => Some(Message::WindowOpen(id)), + iced::Event::Window(iced::window::Event::Focused) => Some(Message::Focus(id)), _ => None, }), Subscription::run_with_id( diff --git a/src/image_container.rs b/src/image_container.rs deleted file mode 100644 index 2f42550..0000000 --- a/src/image_container.rs +++ /dev/null @@ -1,160 +0,0 @@ -use cosmic::iced::ContentFit; -use cosmic::iced::{ - widget::{ - image::{draw, FilterMethod, Handle}, - Container, - }, - Rotation, -}; -use cosmic::iced_core::event::{self, Event}; -use cosmic::iced_core::layout; -use cosmic::iced_core::mouse; -use cosmic::iced_core::overlay; -use cosmic::iced_core::renderer; -use cosmic::iced_core::widget::{Operation, Tree}; -use cosmic::iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget}; -use cosmic::{Renderer, Theme}; - -pub struct ImageContainer<'a, Message> { - container: Container<'a, Message, Theme, Renderer>, - image_opt: Option, - content_fit: ContentFit, -} - -impl<'a, Message> ImageContainer<'a, Message> { - pub fn new(container: Container<'a, Message, Theme, Renderer>) -> Self { - Self { - container, - image_opt: None, - content_fit: ContentFit::None, - } - } - - pub fn image(mut self, image: Handle) -> Self { - self.image_opt = Some(image); - self - } - - pub fn content_fit(mut self, content_fit: ContentFit) -> Self { - self.content_fit = content_fit; - self - } -} - -impl<'a, Message> Widget for ImageContainer<'a, Message> { - fn children(&self) -> Vec { - self.container.children() - } - - fn diff(&mut self, tree: &mut Tree) { - self.container.diff(tree) - } - - fn size(&self) -> Size { - self.container.size() - } - - fn layout( - &self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.container.layout(tree, renderer, limits) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation<()>, - ) { - self.container.operate(tree, layout, renderer, operation) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) -> event::Status { - self.container.on_event( - tree, event, layout, cursor, renderer, clipboard, shell, viewport, - ) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.container - .mouse_interaction(tree, layout, cursor, viewport, renderer) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - renderer_style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - match &self.image_opt { - Some(image) => draw( - renderer, - layout, - image, - self.content_fit, - FilterMethod::Linear, - Rotation::default(), - 1., - [0.0, 0.0, 0.0, 0.0], - ), - None => {} - } - - use cosmic::iced_renderer::core::Renderer as IcedRenderer; - renderer.with_layer(layout.bounds(), |renderer| { - self.container.draw( - tree, - renderer, - theme, - renderer_style, - layout, - cursor, - viewport, - ) - }); - } - - fn overlay<'b>( - &'b mut self, - state: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - translation: cosmic::iced::Vector, - ) -> Option> { - self.container.overlay(state, layout, renderer, translation) - } -} - -impl<'a, Message> From> for Element<'a, Message, Theme, Renderer> -where - Message: 'a, -{ - fn from(container: ImageContainer<'a, Message>) -> Element<'a, Message, Theme, Renderer> { - Element::new(container) - } -} diff --git a/src/lib.rs b/src/lib.rs index bbc4be1..cb2473f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,6 @@ pub mod greeter; pub mod locker; -mod image_container; mod localize; #[cfg(feature = "logind")] diff --git a/src/locker.rs b/src/locker.rs index 7688c0b..16bc877 100644 --- a/src/locker.rs +++ b/src/locker.rs @@ -1,7 +1,10 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only -use cosmic::app::{Action, Core, Settings, Task}; +use cosmic::app::{Core, Settings, Task}; +use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity; +use cosmic::iced::{Point, Rectangle, Size}; +use cosmic::iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings; use cosmic::surface; use cosmic::{ executor, @@ -74,12 +77,15 @@ pub fn main(current_user: pwd::Passwd) -> Result<(), Box> log::error!("failed to create cosmic-bg state helper: {:?}", err); } } + let fallback_background = + widget::image::Handle::from_bytes(include_bytes!("../res/background.jpg").as_slice()); let flags = Flags { current_user, icon_opt, lockfile_opt: lockfile_opt(), wallpapers, + fallback_background, }; let settings = Settings::default().no_main_window(true); @@ -205,6 +211,7 @@ pub struct Flags { icon_opt: Option, lockfile_opt: Option, wallpapers: Vec<(String, cosmic_bg_config::Source)>, + fallback_background: widget::image::Handle, } /// Messages that are used specifically by our [`App`]. @@ -215,11 +222,12 @@ pub enum Message { SessionLockEvent(SessionLockEvent), Channel(mpsc::Sender), BackgroundState(cosmic_bg_config::state::State), + Focus(SurfaceId), Inhibit(Arc), NetworkIcon(Option<&'static str>), PowerInfo(Option<(String, f64)>), Prompt(String, bool, Option), - Submit, + Submit(String), Surface(surface::Action), Suspend, Error(String), @@ -240,11 +248,13 @@ pub struct App { core: Core, 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, + text_input_ids: HashMap, inhibit_opt: Option>, network_icon_opt: Option<&'static str>, power_info_opt: Option<(String, f64)>, @@ -254,364 +264,7 @@ pub struct App { } impl App { - //TODO: cache wallpapers by source? - fn update_wallpapers(&mut self) { - for (output, surface_id) in self.surface_ids.iter() { - if self.surface_images.contains_key(surface_id) { - continue; - } - - let output_name = match self.surface_names.get(surface_id) { - Some(some) => some, - None => continue, - }; - - log::info!("updating wallpaper for {:?}", output_name); - - for (wallpaper_output_name, wallpaper_source) in self.flags.wallpapers.iter() { - if wallpaper_output_name == output_name { - match wallpaper_source { - cosmic_bg_config::Source::Path(path) => { - match fs::read(path) { - Ok(bytes) => { - let image = widget::image::Handle::from_bytes(bytes); - self.surface_images.insert(*surface_id, image); - //TODO: what to do about duplicates? - break; - } - Err(err) => { - log::warn!( - "output {}: failed to load wallpaper {:?}: {:?}", - output.id(), - path, - err - ); - } - } - } - cosmic_bg_config::Source::Color(color) => { - //TODO: support color sources - log::warn!("output {}: unsupported source {:?}", output.id(), color); - } - } - } - } - } - } -} - -/// Implement [`cosmic::Application`] to integrate with COSMIC. -impl cosmic::Application for App { - /// Default async executor to use with the app. - type Executor = executor::Default; - - /// Argument received [`cosmic::Application::new`]. - type Flags = Flags; - - /// Message type specific to our [`App`]. - type Message = Message; - - /// The unique application ID to supply to the window manager. - const APP_ID: &'static str = "com.system76.CosmicGreeter"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.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; - core.window.sharp_corners = true; - core.window.show_maximize = false; - core.window.show_minimize = false; - core.window.use_template = false; - - let already_locked = match flags.lockfile_opt { - Some(ref lockfile) => lockfile.exists(), - None => false, - }; - - let mut app = App { - core, - flags, - state: State::Unlocked, - surface_ids: HashMap::new(), - active_surface_id_opt: None, - surface_images: HashMap::new(), - surface_names: HashMap::new(), - text_input_ids: HashMap::new(), - inhibit_opt: None, - network_icon_opt: None, - power_info_opt: None, - value_tx_opt: None, - prompt_opt: None, - error_opt: None, - }; - - let command = if cfg!(feature = "logind") { - if already_locked { - // Recover previously locked state - log::info!("recovering previous locked state"); - app.state = State::Locking; - lock() - } else { - // When logind feature is used, wait for lock signal - Task::none() - } - } else { - // When logind feature not used, lock immediately - log::info!("locking immediately"); - app.state = State::Locking; - lock() - }; - - (app, command) - } - - /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Task { - match message { - Message::None => {} - Message::OutputEvent(output_event, output) => { - match output_event { - OutputEvent::Created(output_info_opt) => { - log::info!("output {}: created", output.id()); - - let surface_id = SurfaceId::unique(); - - match self.surface_ids.insert(output.clone(), surface_id) { - Some(old_surface_id) => { - //TODO: remove old surface? - log::warn!( - "output {}: already had surface ID {:?}", - output.id(), - old_surface_id - ); - return Task::none(); - } - None => {} - } - - 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_images.remove(&surface_id); - self.update_wallpapers(); - } - None => { - log::warn!("output {}: no output name", output.id()); - } - }, - None => { - log::warn!("output {}: no output info", output.id()); - } - } - - let text_input_id = widget::Id::unique(); - self.text_input_ids - .insert(surface_id, text_input_id.clone()); - - if matches!(self.state, State::Locked) { - return Task::batch([ - get_lock_surface(surface_id, output), - widget::text_input::focus(text_input_id), - ]); - } - } - OutputEvent::Removed => { - log::info!("output {}: removed", output.id()); - match self.surface_ids.remove(&output) { - Some(surface_id) => { - self.surface_images.remove(&surface_id); - self.surface_names.remove(&surface_id); - self.text_input_ids.remove(&surface_id); - if matches!(self.state, State::Locked) { - return destroy_lock_surface(surface_id); - } - } - None => { - log::warn!("output {}: no surface found", output.id()); - } - } - } - OutputEvent::InfoUpdate(_output_info) => { - log::info!("output {}: info update", output.id()); - } - } - } - Message::SessionLockEvent(session_lock_event) => match session_lock_event { - SessionLockEvent::Focused(_, surface_id) => { - log::info!("focus surface {:?}", surface_id); - self.active_surface_id_opt = Some(surface_id); - if let Some(text_input_id) = self.text_input_ids.get(&surface_id) { - return widget::text_input::focus(text_input_id.clone()); - } - } - SessionLockEvent::Locked => { - log::info!("session locked"); - self.state = State::Locked; - // Allow suspend - self.inhibit_opt = None; - // Create lock surfaces - let mut commands = Vec::with_capacity(self.surface_ids.len()); - for (output, surface_id) in self.surface_ids.iter() { - commands.push(get_lock_surface(*surface_id, output.clone())); - } - return Task::batch(commands); - } - SessionLockEvent::Unlocked => { - log::info!("session unlocked"); - self.state = State::Unlocked; - if cfg!(feature = "logind") { - // When using logind feature, stick around for more lock signals - } else { - // When not using logind feature, exit immediately after unlocking - //TODO: cleaner method to exit? - process::exit(0); - } - } - //TODO: handle finished signal - _ => {} - }, - Message::Channel(value_tx) => { - self.value_tx_opt = Some(value_tx); - } - Message::BackgroundState(background_state) => { - self.flags.wallpapers = background_state.wallpapers; - self.surface_images.clear(); - self.update_wallpapers(); - } - 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::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.text_input_ids.get(&surface_id) { - return widget::text_input::focus(text_input_id.clone()); - } - } - } - } - Message::Submit => match self.prompt_opt.take() { - Some((_prompt, _secret, value_opt)) => match value_opt { - Some(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) - }); - } - None => log::warn!("tried to submit when value_tx_opt not set"), - }, - None => log::warn!("tried to submit without value"), - }, - None => log::warn!("tried to submit without prompt"), - }, - Message::Suspend => { - #[cfg(feature = "logind")] - return cosmic::task::future(async move { - match crate::logind::suspend().await { - Ok(()) => cosmic::action::none(), - Err(err) => { - log::error!("failed to suspend: {:?}", err); - cosmic::Action::App(Message::Error(err.to_string())) - } - } - }); - } - Message::Error(error) => { - self.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; - // Clear value_tx - self.value_tx_opt = None; - // Try to create lockfile when locking - if let Some(ref lockfile) = self.flags.lockfile_opt { - if let Err(err) = fs::File::create(lockfile) { - log::warn!("failed to create lockfile {:?}: {}", lockfile, err); - } - } - // Tell compositor to lock - return lock(); - } - State::Unlocking => { - log::info!("session still unlocking"); - } - State::Locking | State::Locked => { - log::info!("session already locking or locked"); - } - }, - Message::Unlock => { - match self.state { - State::Locked => { - log::info!("sessing unlocking"); - self.state = State::Unlocking; - // Clear errors - self.error_opt = None; - // Clear value_tx - self.value_tx_opt = None; - // Try to delete lockfile when unlocking - if let Some(ref lockfile) = self.flags.lockfile_opt { - if let Err(err) = fs::remove_file(lockfile) { - log::warn!("failed to remove lockfile {:?}: {}", lockfile, err); - } - } - // Destroy lock surfaces - let mut commands = Vec::with_capacity(self.surface_ids.len() + 1); - for (_output, surface_id) in self.surface_ids.iter() { - commands.push(destroy_lock_surface(*surface_id)); - } - // Tell compositor to unlock - commands.push(unlock()); - // Wait to exit until `Unlocked` event, when server has processed unlock - return Task::batch(commands); - } - State::Locking => { - log::info!("session still locking"); - } - State::Unlocking | State::Unlocked => { - log::info!("session already unlocking or unlocked"); - } - } - } - Message::Surface(a) => { - return cosmic::task::message(cosmic::Action::Cosmic( - cosmic::app::Action::Surface(a), - )); - } - } - Task::none() - } - - // Not used for layer surface window - fn view(&self) -> Element { - unimplemented!() - } - - /// Creates a view after each update. - fn view_window(&self, surface_id: SurfaceId) -> Element { + fn menu<'a>(&'a self, surface_id: SurfaceId) -> Element<'a, Message> { let left_element = { let date_time_column = { let mut column = widget::column::with_capacity(2).padding(16.0); @@ -717,9 +370,16 @@ impl cosmic::Application for App { match &self.prompt_opt { Some((prompt, secret, value_opt)) => match value_opt { Some(value) => { + let text_input_id = self + .surface_names + .get(&surface_id) + .and_then(|id| self.text_input_ids.get(id)) + .cloned() + .unwrap_or_else(|| cosmic::widget::Id::new("text_input")); + let mut text_input = widget::secure_input( prompt.clone(), - value.clone(), + "", Some(Message::Prompt( prompt.clone(), !*secret, @@ -727,12 +387,9 @@ impl cosmic::Application for App { )), *secret, ) - .on_input(|value| Message::Prompt(prompt.clone(), *secret, Some(value))) - .on_submit(|_| Message::Submit); - - if let Some(text_input_id) = self.text_input_ids.get(&surface_id) { - text_input = text_input.id(text_input_id.clone()); - } + .id(text_input_id) + .manage_value(true) + .on_submit(|v| Message::Submit(v)); if *secret { text_input = text_input.password() @@ -756,47 +413,529 @@ impl cosmic::Application for App { .width(Length::Fill) }; - crate::image_container::ImageContainer::new( - widget::container( - widget::layer_container( - iced::widget::row![left_element, right_element] - .align_y(alignment::Alignment::Center), - ) - .layer(cosmic::cosmic_theme::Layer::Background) - .padding(16) - .class(cosmic::theme::Container::Custom(Box::new( - |theme: &cosmic::Theme| { - // Use background appearance as the base - let mut appearance = widget::container::Catalog::style( - theme, - &cosmic::theme::Container::Background, - ); - appearance.border = iced::Border::default().rounded(16.0); - appearance - }, - ))) - .width(Length::Fixed(800.0)), + widget::container( + widget::layer_container( + iced::widget::row![left_element, right_element] + .align_y(alignment::Alignment::Center), ) - .padding([32.0, 0.0, 0.0, 0.0]) + .layer(cosmic::cosmic_theme::Layer::Background) + .padding(16) + .class(cosmic::theme::Container::Custom(Box::new( + |theme: &cosmic::Theme| { + // Use background appearance as the base + let mut appearance = widget::container::Catalog::style( + theme, + &cosmic::theme::Container::Background, + ); + appearance.border = iced::Border::default().rounded(16.0); + appearance + }, + ))) + .width(Length::Fill) + .height(Length::Shrink), + ) + .padding([32.0, 0.0, 0.0, 0.0]) + .width(Length::Fill) + .height(Length::Fill) + .align_x(alignment::Horizontal::Center) + .align_y(alignment::Vertical::Top) + .class(cosmic::theme::Container::Transparent) + .into() + } + + //TODO: cache wallpapers by source? + fn update_wallpapers(&mut self) { + for (output, surface_id) in self.surface_ids.iter() { + if self.surface_images.contains_key(surface_id) { + continue; + } + + let output_name = match self.surface_names.get(surface_id) { + Some(some) => some, + None => continue, + }; + + log::info!("updating wallpaper for {:?}", output_name); + + for (wallpaper_output_name, wallpaper_source) in self.flags.wallpapers.iter() { + if wallpaper_output_name == output_name { + match wallpaper_source { + cosmic_bg_config::Source::Path(path) => { + match fs::read(path) { + Ok(bytes) => { + let image = widget::image::Handle::from_bytes(bytes); + self.surface_images.insert(*surface_id, image); + //TODO: what to do about duplicates? + break; + } + Err(err) => { + log::warn!( + "output {}: failed to load wallpaper {:?}: {:?}", + output.id(), + path, + err + ); + } + } + } + cosmic_bg_config::Source::Color(color) => { + //TODO: support color sources + log::warn!("output {}: unsupported source {:?}", output.id(), color); + } + } + } + } + } + } +} + +/// Implement [`cosmic::Application`] to integrate with COSMIC. +impl cosmic::Application for App { + /// Default async executor to use with the app. + type Executor = executor::Default; + + /// Argument received [`cosmic::Application::new`]. + type Flags = Flags; + + /// Message type specific to our [`App`]. + type Message = Message; + + /// The unique application ID to supply to the window manager. + const APP_ID: &'static str = "com.system76.CosmicGreeter"; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.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; + + let already_locked = match flags.lockfile_opt { + Some(ref lockfile) => lockfile.exists(), + None => false, + }; + + let mut app = App { + core, + 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, + }; + + let command = if cfg!(feature = "logind") { + if already_locked { + // Recover previously locked state + log::info!("recovering previous locked state"); + app.state = State::Locking; + lock() + } else { + // When logind feature is used, wait for lock signal + Task::none() + } + } else { + // When logind feature not used, lock immediately + log::info!("locking immediately"); + app.state = State::Locking; + lock() + }; + + (app, command) + } + + /// Handle application events here. + fn update(&mut self, message: Self::Message) -> Task { + match message { + Message::None => {} + Message::OutputEvent(output_event, output) => { + match output_event { + OutputEvent::Created(output_info_opt) => { + log::info!("output {}: created", output.id()); + + let surface_id = SurfaceId::unique(); + let subsurface_id = SurfaceId::unique(); + + match self.surface_ids.insert(output.clone(), surface_id) { + Some(old_surface_id) => { + //TODO: remove old surface? + log::warn!( + "output {}: already had surface ID {:?}", + output.id(), + old_surface_id + ); + return Task::none(); + } + None => {} + } + let size = if let Some((w, h)) = + output_info_opt.as_ref().and_then(|info| info.logical_size) + { + Some((Some(w as u32), Some(h as u32))) + } else { + Some((None, None)) + }; + match output_info_opt { + Some(output_info) => match output_info.name { + Some(output_name) => { + self.output_names + .insert(output.clone(), output_name.clone()); + self.surface_names.insert(surface_id, output_name.clone()); + self.surface_names + .insert(subsurface_id, output_name.clone()); + self.surface_images.remove(&surface_id); + self.update_wallpapers(); + let text_input_id = + widget::Id::new(format!("input-{output_name}",)); + self.text_input_ids + .insert(output_name.clone(), text_input_id.clone()); + } + None => { + log::warn!("output {}: no output name", output.id()); + } + }, + None => { + log::warn!("output {}: no output info", output.id()); + } + } + + let unwrapped_size = size + .map(|s| (s.0.unwrap_or(1920), s.1.unwrap_or(1080))) + .unwrap_or((1920, 1080)); + let (loc, sub_size) = if unwrapped_size.0 > 800 { + ( + Point::new(unwrapped_size.0 as f32 / 2. - 400., 32.), + Size::new(800., unwrapped_size.1 as f32 - 32.), + ) + } else { + ( + Point::new(0., 32.), + Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32 - 32.), + ) + }; + self.subsurface_rects + .insert(output.clone(), Rectangle::new(loc, sub_size)); + + let msg = cosmic::surface::action::subsurface( + move |_: &mut App| SctkSubsurfaceSettings { + parent: surface_id, + id: subsurface_id, + loc, + size: Some(sub_size), + z: 10, + steal_keyboard_focus: true, + gravity: Gravity::BottomRight, + offset: (0, 0), + input_zone: None, + }, + Some(Box::new(move |app: &App| { + app.menu(subsurface_id).map(cosmic::Action::App) + })), + ); + + if matches!(self.state, State::Locked) { + return Task::batch([ + get_lock_surface(surface_id, output), + cosmic::task::message(cosmic::Action::Cosmic( + cosmic::app::Action::Surface(msg), + )), + ]); + } + } + OutputEvent::Removed => { + log::info!("output {}: removed", output.id()); + match self.surface_ids.remove(&output) { + Some(surface_id) => { + 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); + } + if matches!(self.state, State::Locked) { + return destroy_lock_surface(surface_id); + } + } + None => { + log::warn!("output {}: no surface found", output.id()); + } + } + } + OutputEvent::InfoUpdate(info) => { + let size = if let Some((w, h)) = info.logical_size { + Some((Some(w as u32), Some(h as u32))) + } else { + Some((None, None)) + }; + let unwrapped_size = size + .map(|s| (s.0.unwrap_or(1920), s.1.unwrap_or(1080))) + .unwrap_or((1920, 1080)); + let (loc, sub_size) = if unwrapped_size.0 > 800 { + ( + Point::new(unwrapped_size.0 as f32 / 2. - 400., 32.), + Size::new(800., unwrapped_size.1 as f32 - 32.), + ) + } else { + (Point::ORIGIN, Size::new(1920., 1080.)) + }; + self.subsurface_rects + .insert(output.clone(), Rectangle::new(loc, sub_size)); + + log::info!("output {}: info update", output.id()); + } + } + } + Message::SessionLockEvent(session_lock_event) => match session_lock_event { + SessionLockEvent::Focused(..) => {} + SessionLockEvent::Locked => { + log::info!("session locked"); + if matches!(self.state, State::Locked) { + return Task::none(); + } + self.state = State::Locked; + // Allow suspend + self.inhibit_opt = None; + // Create lock surfaces + + let mut commands = Vec::with_capacity(self.surface_ids.len()); + for (output, surface_id) in self.surface_ids.iter() { + commands.push(get_lock_surface(*surface_id, output.clone())); + + if let Some((rect, name)) = self + .subsurface_rects + .get(output) + .copied() + .zip(self.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()); + let msg = cosmic::surface::action::subsurface( + move |_: &mut App| SctkSubsurfaceSettings { + parent: surface_id, + id: subsurface_id, + loc: Point::new(rect.x, rect.y), + size: Some(Size::new(rect.width, rect.height)), + z: 10, + steal_keyboard_focus: true, + gravity: Gravity::BottomRight, + offset: (0, 0), + input_zone: None, + }, + Some(Box::new(move |app: &App| { + app.menu(subsurface_id).map(cosmic::Action::App) + })), + ); + commands.push(cosmic::task::message(cosmic::Action::Cosmic( + cosmic::app::Action::Surface(msg), + ))); + } else { + log::error!("no rectangle for subsurface..."); + } + } + return Task::batch(commands); + } + SessionLockEvent::Unlocked => { + log::info!("session unlocked"); + self.state = State::Unlocked; + + let mut commands = Vec::new(); + for (_output, surface_id) in self.surface_ids.iter() { + self.surface_names.remove(surface_id); + + commands.push(destroy_lock_surface(*surface_id)); + } + if cfg!(feature = "logind") { + return Task::batch(commands); + // When using logind feature, stick around for more lock signals + } else { + // When not using logind feature, exit immediately after unlocking + //TODO: cleaner method to exit? + process::exit(0); + } + } + //TODO: handle finished signal + _ => {} + }, + Message::Channel(value_tx) => { + self.value_tx_opt = Some(value_tx); + } + Message::BackgroundState(background_state) => { + self.flags.wallpapers = background_state.wallpapers; + self.surface_images.clear(); + self.update_wallpapers(); + } + 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)); + 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)) + { + log::error!("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) + }); + } + None => log::warn!("tried to submit when value_tx_opt not set"), + }, + Message::Suspend => { + #[cfg(feature = "logind")] + return cosmic::task::future(async move { + match crate::logind::suspend().await { + Ok(()) => cosmic::action::none(), + Err(err) => { + log::error!("failed to suspend: {:?}", err); + cosmic::Action::App(Message::Error(err.to_string())) + } + } + }); + } + Message::Error(error) => { + self.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; + // Clear value_tx + self.value_tx_opt = None; + // Try to create lockfile when locking + if let Some(ref lockfile) = self.flags.lockfile_opt { + if let Err(err) = fs::File::create(lockfile) { + log::warn!("failed to create lockfile {:?}: {}", lockfile, err); + } + } + // Tell compositor to lock + return lock(); + } + State::Unlocking => { + log::info!("session still unlocking"); + } + State::Locking | State::Locked => { + log::info!("session already locking or locked"); + } + }, + Message::Unlock => { + match self.state { + State::Locked => { + log::info!("sessing unlocking"); + self.state = State::Unlocking; + // Clear errors + self.error_opt = None; + // Clear value_tx + self.value_tx_opt = None; + // Try to delete lockfile when unlocking + if let Some(ref lockfile) = self.flags.lockfile_opt { + if let Err(err) = fs::remove_file(lockfile) { + log::warn!("failed to remove lockfile {:?}: {}", lockfile, err); + } + } + + // Destroy lock surfaces + let mut commands = Vec::with_capacity(self.surface_ids.len() + 1); + // Tell compositor to unlock + commands.push(unlock()); + + // Wait to exit until `Unlocked` event, when server has processed unlock + return Task::batch(commands); + } + State::Locking => { + log::info!("session still locking"); + } + State::Unlocking | State::Unlocked => { + log::info!("session already unlocking or unlocked"); + } + } + } + Message::Surface(a) => { + return cosmic::task::message(cosmic::Action::Cosmic( + cosmic::app::Action::Surface(a), + )); + } + } + Task::none() + } + + // Not used for layer surface window + fn view(&self) -> Element { + unimplemented!() + } + + /// Creates a view after each update. + fn view_window(&self, surface_id: SurfaceId) -> Element { + let img = self + .surface_images + .get(&surface_id) + .unwrap_or(&self.flags.fallback_background); + widget::image(img) + .content_fit(iced::ContentFit::Cover) .width(Length::Fill) .height(Length::Fill) - .align_x(alignment::Horizontal::Center) - .align_y(alignment::Vertical::Top) - .class(cosmic::theme::Container::Transparent), - ) - .image(match self.surface_images.get(&surface_id) { - Some(some) => some.clone(), - //TODO: default image - None => widget::image::Handle::from_rgba(1, 1, vec![0x00, 0x00, 0x00, 0xFF]), - }) - .content_fit(iced::ContentFit::Cover) - .into() + .into() } fn subscription(&self) -> Subscription { let mut subscriptions = Vec::with_capacity(7); - subscriptions.push(event::listen_with(|event, _, _| match event { + subscriptions.push(event::listen_with(|event, _, id| match event { iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland( wayland_event, )) => match wayland_event { @@ -806,6 +945,7 @@ impl cosmic::Application for App { WaylandEvent::SessionLock(evt) => Some(Message::SessionLockEvent(evt)), _ => None, }, + iced::Event::Window(iced::window::Event::Focused) => Some(Message::Focus(id)), _ => None, }));