refactor: lock surface subsurface menu

This commit is contained in:
Ashley Wulber 2025-03-12 16:07:57 -04:00 • committed by Jeremy Soller
parent b84ee1827c
commit d6b94406e6
5 changed files with 573 additions and 616 deletions

30
Cargo.lock generated
View file

@ -1046,7 +1046,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-config" name = "cosmic-config"
version = "0.1.0" 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 = [ dependencies = [
"atomicwrites", "atomicwrites",
"calloop 0.14.2", "calloop 0.14.2",
@ -1069,7 +1069,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-config-derive" name = "cosmic-config-derive"
version = "0.1.0" 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 = [ dependencies = [
"quote", "quote",
"syn 1.0.109", "syn 1.0.109",
@ -1212,7 +1212,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-theme" name = "cosmic-theme"
version = "0.1.0" 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 = [ dependencies = [
"almost", "almost",
"cosmic-config", "cosmic-config",
@ -2487,7 +2487,7 @@ dependencies = [
[[package]] [[package]]
name = "iced" name = "iced"
version = "0.14.0-dev" 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 = [ dependencies = [
"dnd", "dnd",
"iced_accessibility", "iced_accessibility",
@ -2505,7 +2505,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_accessibility" name = "iced_accessibility"
version = "0.1.0" 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 = [ dependencies = [
"accesskit", "accesskit",
"accesskit_winit", "accesskit_winit",
@ -2514,7 +2514,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_core" name = "iced_core"
version = "0.14.0-dev" 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 = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"bytes", "bytes",
@ -2538,7 +2538,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_futures" name = "iced_futures"
version = "0.14.0-dev" 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 = [ dependencies = [
"futures", "futures",
"iced_core", "iced_core",
@ -2564,7 +2564,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_graphics" name = "iced_graphics"
version = "0.14.0-dev" 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 = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"bytemuck", "bytemuck",
@ -2586,7 +2586,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_renderer" name = "iced_renderer"
version = "0.14.0-dev" 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 = [ dependencies = [
"iced_graphics", "iced_graphics",
"iced_tiny_skia", "iced_tiny_skia",
@ -2598,7 +2598,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_runtime" name = "iced_runtime"
version = "0.14.0-dev" 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 = [ dependencies = [
"bytes", "bytes",
"cosmic-client-toolkit", "cosmic-client-toolkit",
@ -2613,7 +2613,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_tiny_skia" name = "iced_tiny_skia"
version = "0.14.0-dev" 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 = [ dependencies = [
"bytemuck", "bytemuck",
"cosmic-text", "cosmic-text",
@ -2629,7 +2629,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_wgpu" name = "iced_wgpu"
version = "0.14.0-dev" 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 = [ dependencies = [
"as-raw-xcb-connection", "as-raw-xcb-connection",
"bitflags 2.8.0", "bitflags 2.8.0",
@ -2660,7 +2660,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_widget" name = "iced_widget"
version = "0.14.0-dev" 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 = [ dependencies = [
"cosmic-client-toolkit", "cosmic-client-toolkit",
"dnd", "dnd",
@ -2678,7 +2678,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_winit" name = "iced_winit"
version = "0.14.0-dev" 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 = [ dependencies = [
"cosmic-client-toolkit", "cosmic-client-toolkit",
"dnd", "dnd",
@ -3144,7 +3144,7 @@ checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
[[package]] [[package]]
name = "libcosmic" name = "libcosmic"
version = "0.1.0" 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 = [ dependencies = [
"apply", "apply",
"ashpd 0.9.2", "ashpd 0.9.2",

View file

@ -5,7 +5,7 @@ mod ipc;
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity; 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_core::{image, window};
use cosmic::iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings; use cosmic::iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings;
use cosmic::surface; use cosmic::surface;
@ -400,11 +400,11 @@ pub enum Message {
DropdownToggle(Dropdown), DropdownToggle(Dropdown),
Error(String), Error(String),
Exit, Exit,
Focus(SurfaceId),
// Sets channel used to communicate with the greetd IPC subscription. // Sets channel used to communicate with the greetd IPC subscription.
GreetdChannel(tokio::sync::mpsc::Sender<Request>), GreetdChannel(tokio::sync::mpsc::Sender<Request>),
Heartbeat, Heartbeat,
KeyboardLayout(usize), KeyboardLayout(usize),
LayerEvent(LayerEvent, SurfaceId),
Login, Login,
NetworkIcon(Option<&'static str>), NetworkIcon(Option<&'static str>),
None, None,
@ -419,7 +419,6 @@ pub enum Message {
Surface(surface::Action), Surface(surface::Action),
Suspend, Suspend,
Username(String), Username(String),
WindowOpen(SurfaceId),
} }
/// The [`App`] stores application-specific state. /// The [`App`] stores application-specific state.
@ -718,9 +717,6 @@ impl App {
) )
.id(text_input_id) .id(text_input_id)
.manage_value(true) .manage_value(true)
// .on_input(|value| {
// Message::Prompt(prompt.clone(), *secret, Some(value))
// })
.on_submit(|v| Message::Auth(Some(v))); .on_submit(|v| Message::Auth(Some(v)));
if let Some(text_input_id) = self 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.), Size::new(800., unwrapped_size.1 as f32 - 32.),
) )
} else { } 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( self.window_size.insert(
surface_id, surface_id,
Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32), Size::new(unwrapped_size.0 as f32, unwrapped_size.1 as f32),
); );
dbg!(loc, size);
let msg = cosmic::surface::action::subsurface( let msg = cosmic::surface::action::subsurface(
move |_: &mut App| SctkSubsurfaceSettings { move |_: &mut App| SctkSubsurfaceSettings {
parent: surface_id, 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) => { Message::Socket(socket_state) => {
self.socket_state = socket_state; self.socket_state = socket_state;
match &self.socket_state { match &self.socket_state {
@ -1481,22 +1466,17 @@ impl cosmic::Application for App {
cosmic::app::Action::Surface(a), 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| { Message::Focus(surface_id) => {
if self self.active_surface_id_opt = Some(surface_id);
.active_surface_id_opt if let Some(text_input_id) = self
.and_then(|active_id| self.surface_names.get(&active_id)) .surface_names
== Some(id) .get(&surface_id)
.and_then(|id| self.text_input_ids.get(id))
{ {
self.text_input_ids.get(id)
} else {
None
}
}) {
return widget::text_input::focus(text_input_id.clone()); return widget::text_input::focus(text_input_id.clone());
} }
} }
Message::WindowOpen(_) => {}
} }
Task::none() Task::none()
} }
@ -1530,12 +1510,10 @@ impl cosmic::Application for App {
WaylandEvent::Output(output_event, output) => { WaylandEvent::Output(output_event, output) => {
Some(Message::OutputEvent(output_event, output)) Some(Message::OutputEvent(output_event, output))
} }
WaylandEvent::Layer(layer_event, _surface, surface_id) => {
Some(Message::LayerEvent(layer_event, surface_id))
}
_ => None, _ => None,
}, },
iced::Event::Window(window::Event::Opened { .. }) => Some(Message::WindowOpen(id)), iced::Event::Window(iced::window::Event::Focused) => Some(Message::Focus(id)),
_ => None, _ => None,
}), }),
Subscription::run_with_id( Subscription::run_with_id(

View file

@ -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<Handle>,
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<Message, Theme, Renderer> for ImageContainer<'a, Message> {
fn children(&self) -> Vec<Tree> {
self.container.children()
}
fn diff(&mut self, tree: &mut Tree) {
self.container.diff(tree)
}
fn size(&self) -> Size<Length> {
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<overlay::Element<'b, Message, Theme, Renderer>> {
self.container.overlay(state, layout, renderer, translation)
}
}
impl<'a, Message> From<ImageContainer<'a, Message>> for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
{
fn from(container: ImageContainer<'a, Message>) -> Element<'a, Message, Theme, Renderer> {
Element::new(container)
}
}

View file

@ -4,7 +4,6 @@
pub mod greeter; pub mod greeter;
pub mod locker; pub mod locker;
mod image_container;
mod localize; mod localize;
#[cfg(feature = "logind")] #[cfg(feature = "logind")]

View file

@ -1,7 +1,10 @@
// Copyright 2023 System76 <info@system76.com> // Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only // 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::surface;
use cosmic::{ use cosmic::{
executor, executor,
@ -74,12 +77,15 @@ pub fn main(current_user: pwd::Passwd) -> Result<(), Box<dyn std::error::Error>>
log::error!("failed to create cosmic-bg state helper: {:?}", err); 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 { let flags = Flags {
current_user, current_user,
icon_opt, icon_opt,
lockfile_opt: lockfile_opt(), lockfile_opt: lockfile_opt(),
wallpapers, wallpapers,
fallback_background,
}; };
let settings = Settings::default().no_main_window(true); let settings = Settings::default().no_main_window(true);
@ -205,6 +211,7 @@ pub struct Flags {
icon_opt: Option<widget::image::Handle>, icon_opt: Option<widget::image::Handle>,
lockfile_opt: Option<PathBuf>, lockfile_opt: Option<PathBuf>,
wallpapers: Vec<(String, cosmic_bg_config::Source)>, wallpapers: Vec<(String, cosmic_bg_config::Source)>,
fallback_background: widget::image::Handle,
} }
/// Messages that are used specifically by our [`App`]. /// Messages that are used specifically by our [`App`].
@ -215,11 +222,12 @@ pub enum Message {
SessionLockEvent(SessionLockEvent), SessionLockEvent(SessionLockEvent),
Channel(mpsc::Sender<String>), Channel(mpsc::Sender<String>),
BackgroundState(cosmic_bg_config::state::State), BackgroundState(cosmic_bg_config::state::State),
Focus(SurfaceId),
Inhibit(Arc<OwnedFd>), Inhibit(Arc<OwnedFd>),
NetworkIcon(Option<&'static str>), NetworkIcon(Option<&'static str>),
PowerInfo(Option<(String, f64)>), PowerInfo(Option<(String, f64)>),
Prompt(String, bool, Option<String>), Prompt(String, bool, Option<String>),
Submit, Submit(String),
Surface(surface::Action), Surface(surface::Action),
Suspend, Suspend,
Error(String), Error(String),
@ -240,11 +248,13 @@ pub struct App {
core: Core, core: Core,
flags: Flags, flags: Flags,
state: State, state: State,
output_names: HashMap<WlOutput, String>,
surface_ids: HashMap<WlOutput, SurfaceId>, surface_ids: HashMap<WlOutput, SurfaceId>,
subsurface_rects: HashMap<WlOutput, Rectangle>,
active_surface_id_opt: Option<SurfaceId>, active_surface_id_opt: Option<SurfaceId>,
surface_images: HashMap<SurfaceId, widget::image::Handle>, surface_images: HashMap<SurfaceId, widget::image::Handle>,
surface_names: HashMap<SurfaceId, String>, surface_names: HashMap<SurfaceId, String>,
text_input_ids: HashMap<SurfaceId, widget::Id>, text_input_ids: HashMap<String, widget::Id>,
inhibit_opt: Option<Arc<OwnedFd>>, inhibit_opt: Option<Arc<OwnedFd>>,
network_icon_opt: Option<&'static str>, network_icon_opt: Option<&'static str>,
power_info_opt: Option<(String, f64)>, power_info_opt: Option<(String, f64)>,
@ -254,364 +264,7 @@ pub struct App {
} }
impl App { impl App {
//TODO: cache wallpapers by source? fn menu<'a>(&'a self, surface_id: SurfaceId) -> Element<'a, Message> {
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<Self::Message>) {
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<Self::Message> {
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<Self::Message> {
unimplemented!()
}
/// Creates a view after each update.
fn view_window(&self, surface_id: SurfaceId) -> Element<Self::Message> {
let left_element = { let left_element = {
let date_time_column = { let date_time_column = {
let mut column = widget::column::with_capacity(2).padding(16.0); let mut column = widget::column::with_capacity(2).padding(16.0);
@ -717,9 +370,16 @@ impl cosmic::Application for App {
match &self.prompt_opt { match &self.prompt_opt {
Some((prompt, secret, value_opt)) => match value_opt { Some((prompt, secret, value_opt)) => match value_opt {
Some(value) => { 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( let mut text_input = widget::secure_input(
prompt.clone(), prompt.clone(),
value.clone(), "",
Some(Message::Prompt( Some(Message::Prompt(
prompt.clone(), prompt.clone(),
!*secret, !*secret,
@ -727,12 +387,9 @@ impl cosmic::Application for App {
)), )),
*secret, *secret,
) )
.on_input(|value| Message::Prompt(prompt.clone(), *secret, Some(value))) .id(text_input_id)
.on_submit(|_| Message::Submit); .manage_value(true)
.on_submit(|v| Message::Submit(v));
if let Some(text_input_id) = self.text_input_ids.get(&surface_id) {
text_input = text_input.id(text_input_id.clone());
}
if *secret { if *secret {
text_input = text_input.password() text_input = text_input.password()
@ -756,7 +413,6 @@ impl cosmic::Application for App {
.width(Length::Fill) .width(Length::Fill)
}; };
crate::image_container::ImageContainer::new(
widget::container( widget::container(
widget::layer_container( widget::layer_container(
iced::widget::row![left_element, right_element] iced::widget::row![left_element, right_element]
@ -775,28 +431,511 @@ impl cosmic::Application for App {
appearance appearance
}, },
))) )))
.width(Length::Fixed(800.0)), .width(Length::Fill)
.height(Length::Shrink),
) )
.padding([32.0, 0.0, 0.0, 0.0]) .padding([32.0, 0.0, 0.0, 0.0])
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.align_x(alignment::Horizontal::Center) .align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::Top) .align_y(alignment::Vertical::Top)
.class(cosmic::theme::Container::Transparent), .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<Self::Message>) {
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<Self::Message> {
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.),
) )
.image(match self.surface_images.get(&surface_id) { } else {
Some(some) => some.clone(), (
//TODO: default image Point::new(0., 32.),
None => widget::image::Handle::from_rgba(1, 1, vec![0x00, 0x00, 0x00, 0xFF]), 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<Self::Message> {
unimplemented!()
}
/// Creates a view after each update.
fn view_window(&self, surface_id: SurfaceId) -> Element<Self::Message> {
let img = self
.surface_images
.get(&surface_id)
.unwrap_or(&self.flags.fallback_background);
widget::image(img)
.content_fit(iced::ContentFit::Cover) .content_fit(iced::ContentFit::Cover)
.width(Length::Fill)
.height(Length::Fill)
.into() .into()
} }
fn subscription(&self) -> Subscription<Self::Message> { fn subscription(&self) -> Subscription<Self::Message> {
let mut subscriptions = Vec::with_capacity(7); 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( iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(
wayland_event, wayland_event,
)) => match wayland_event { )) => match wayland_event {
@ -806,6 +945,7 @@ impl cosmic::Application for App {
WaylandEvent::SessionLock(evt) => Some(Message::SessionLockEvent(evt)), WaylandEvent::SessionLock(evt) => Some(Message::SessionLockEvent(evt)),
_ => None, _ => None,
}, },
iced::Event::Window(iced::window::Event::Focused) => Some(Message::Focus(id)),
_ => None, _ => None,
})); }));