diff --git a/src/app.rs b/src/app.rs index c2625e5..0c0eba3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,13 +2,13 @@ use crate::{ centerbox, config::{self, Config}, get_log_spec, - menu::{menu_wrapper, Menu, MenuPosition}, + menu::{menu_wrapper, MenuPosition}, modules::{ self, clipboard, clock::Clock, keyboard_layout::KeyboardLayout, keyboard_submap::KeyboardSubmap, launcher, privacy::PrivacyMessage, settings::Settings, system_info::SystemInfo, title::Title, updates::Updates, workspaces::Workspaces, }, - outputs::Outputs, + outputs::{HasOutput, Outputs}, services::{privacy::PrivacyService, ReadOnlyService, ServiceEvent}, style::ashell_theme, utils, HEIGHT, @@ -17,7 +17,7 @@ use flexi_logger::LoggerHandle; use iced::{ daemon::Appearance, event::{listen_with, wayland::Event as WaylandEvent}, - widget::{row, Row}, + widget::Row, window::Id, Alignment, Color, Element, Length, Subscription, Task, Theme, }; @@ -48,7 +48,7 @@ pub enum MenuType { pub enum Message { None, ConfigChanged(Box), - CloseMenu, + CloseMenu(Id), OpenLauncher, OpenClipboard, Updates(modules::updates::Message), @@ -60,7 +60,7 @@ pub enum Message { Clock(modules::clock::Message), Privacy(modules::privacy::PrivacyMessage), Settings(modules::settings::Message), - // WaylandEvent(WaylandEvent), + WaylandEvent(WaylandEvent), } impl App { @@ -107,19 +107,21 @@ impl App { Message::None => Task::none(), Message::ConfigChanged(config) => { info!("New config: {:?}", config); + let mut tasks = Vec::new(); + if self.config.outputs != config.outputs { + tasks.push(self.outputs.sync(&config.outputs, config.position)); + } self.config = *config; self.logger .set_new_spec(get_log_spec(&self.config.log_level)); - Task::none() - } - Message::CloseMenu => { - // self.menu.close(), - Task::none() + + Task::batch(tasks) } + Message::CloseMenu(id) => self.outputs.close_menu(id), Message::Updates(message) => { if let Some(updates_config) = self.config.updates.as_ref() { - // self.updates.update(message, updates_config, &mut self.menu) - Task::none() + self.updates + .update(message, updates_config, &mut self.outputs) } else { Task::none() } @@ -178,125 +180,134 @@ impl App { }, }, Message::Settings(message) => { - // self.settings - // .update(message, &self.config.settings, &mut self.menu) - Task::none() - } // Message::WaylandEvent(event) => match event { - // WaylandEvent::Output(event, wl_output) => match event { - // iced::event::wayland::OutputEvent::Created(info) => { - // info!("Output created: {:?}", info); - // self.outputs.add( - // &self.config.outputs, - // self.config.position, - // info, - // wl_output, - // ) - // } - // iced::event::wayland::OutputEvent::Removed => { - // info!("Output destroyed"); - // self.outputs.remove(self.config.position, wl_output) - // } - // _ => Task::none(), - // }, - // _ => Task::none(), - // }, + self.settings + .update(message, &self.config.settings, &mut self.outputs) + } + Message::WaylandEvent(event) => match event { + WaylandEvent::Output(event, wl_output) => match event { + iced::event::wayland::OutputEvent::Created(info) => { + info!("Output created: {:?}", info); + let name = info + .as_ref() + .and_then(|info| info.name.as_deref()) + .unwrap_or(""); + + self.outputs.add( + &self.config.outputs, + self.config.position, + name, + wl_output, + ) + } + iced::event::wayland::OutputEvent::Removed => { + info!("Output destroyed"); + self.outputs.remove(self.config.position, wl_output) + } + _ => Task::none(), + }, + _ => Task::none(), + }, } } pub fn view(&self, id: Id) -> Element { - // if self.menu.is_menu(id) { - // match self.menu.get_menu_type_to_render(id) { - // Some(MenuType::Updates) => menu_wrapper( - // self.updates.menu_view().map(Message::Updates), - // MenuPosition::Left, - // self.config.position, - // ), - // Some(MenuType::Settings) => menu_wrapper( - // self.settings - // .menu_view(&self.config.settings) - // .map(Message::Settings), - // MenuPosition::Right, - // self.config.position, - // ), - // None => Row::new().into(), - // } - // } else { - let left = Row::new() - .push_maybe( - self.config - .app_launcher_cmd - .as_ref() - .map(|_| launcher::launcher()), - ) - .push_maybe( - self.config - .clipboard_cmd - .as_ref() - .map(|_| clipboard::clipboard()), - ) - .push_maybe( - self.config - .updates - .as_ref() - .map(|_| self.updates.view().map(Message::Updates)), - ) - .push( - self.workspaces - .view( - &self.config.appearance.workspace_colors, - self.config.appearance.special_workspace_colors.as_deref(), + match self.outputs.has(id) { + Some(HasOutput::Main) => { + let left = Row::new() + .push_maybe( + self.config + .app_launcher_cmd + .as_ref() + .map(|_| launcher::launcher()), ) - .map(Message::Workspaces), - ) - .height(Length::Shrink) - .align_y(Alignment::Center) - .spacing(4); + .push_maybe( + self.config + .clipboard_cmd + .as_ref() + .map(|_| clipboard::clipboard()), + ) + .push_maybe( + self.config + .updates + .as_ref() + .map(|_| self.updates.view(id).map(Message::Updates)), + ) + .push( + self.workspaces + .view( + &self.config.appearance.workspace_colors, + self.config.appearance.special_workspace_colors.as_deref(), + ) + .map(Message::Workspaces), + ) + .height(Length::Shrink) + .align_y(Alignment::Center) + .spacing(4); - let center = Row::new() - .push_maybe(self.window_title.view().map(|v| v.map(Message::Title))) - .spacing(4); + let center = Row::new() + .push_maybe(self.window_title.view().map(|v| v.map(Message::Title))) + .spacing(4); - let right = Row::new() - .push_maybe( - self.system_info - .view(&self.config.system) - .map(|c| c.map(Message::SystemInfo)), - ) - .push_maybe( - self.keyboard_submap - .view(&self.config.keyboard.submap) - .map(|l| l.map(Message::KeyboardSubmap)), - ) - .push_maybe( - self.keyboard_layout - .view(&self.config.keyboard.layout) - .map(|l| l.map(Message::KeyboardLayout)), - ) - .push( - Row::new() - .push( - self.clock - .view(&self.config.clock.format) - .map(Message::Clock), + let right = Row::new() + .push_maybe( + self.system_info + .view(&self.config.system) + .map(|c| c.map(Message::SystemInfo)), ) .push_maybe( - self.privacy - .as_ref() - .and_then(|privacy| privacy.view()) - .map(|e| e.map(Message::Privacy)), + self.keyboard_submap + .view(&self.config.keyboard.submap) + .map(|l| l.map(Message::KeyboardSubmap)), ) - .push(self.settings.view().map(Message::Settings)), - ) - .spacing(4); + .push_maybe( + self.keyboard_layout + .view(&self.config.keyboard.layout) + .map(|l| l.map(Message::KeyboardLayout)), + ) + .push( + Row::new() + .push( + self.clock + .view(&self.config.clock.format) + .map(Message::Clock), + ) + .push_maybe( + self.privacy + .as_ref() + .and_then(|privacy| privacy.view()) + .map(|e| e.map(Message::Privacy)), + ) + .push(self.settings.view(id).map(Message::Settings)), + ) + .spacing(4); - centerbox::Centerbox::new([left.into(), center.into(), right.into()]) - .spacing(4) - .padding([0, 4]) - .width(Length::Fill) - .height(Length::Fixed(HEIGHT as f32)) - .align_items(Alignment::Center) - .into() - // } + centerbox::Centerbox::new([left.into(), center.into(), right.into()]) + .spacing(4) + .padding([0, 4]) + .width(Length::Fill) + .height(Length::Fixed(HEIGHT as f32)) + .align_items(Alignment::Center) + .into() + } + Some(HasOutput::Menu(menu_type)) => match menu_type { + Some(MenuType::Updates) => menu_wrapper( + id, + self.updates.menu_view(id).map(Message::Updates), + MenuPosition::Left, + self.config.position, + ), + Some(MenuType::Settings) => menu_wrapper( + id, + self.settings + .menu_view(id, &self.config.settings) + .map(Message::Settings), + MenuPosition::Right, + self.config.position, + ), + None => Row::new().into(), + }, + None => Row::new().into(), + } } pub fn subscription(&self) -> Subscription { @@ -326,21 +337,21 @@ impl App { ), Some(self.settings.subscription().map(Message::Settings)), Some(config::subscription()), - // Some(listen_with(|evt, _, _| { - // if let iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland( - // evt, - // )) = evt - // { - // if matches!(evt, WaylandEvent::Output(_, _)) { - // debug!("Wayland event: {:?}", evt); - // Some(Message::WaylandEvent(evt)) - // } else { - // None - // } - // } else { - // None - // } - // })), + Some(listen_with(|evt, _, _| { + if let iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland( + evt, + )) = evt + { + if matches!(evt, WaylandEvent::Output(_, _)) { + debug!("Wayland event: {:?}", evt); + Some(Message::WaylandEvent(evt)) + } else { + None + } + } else { + None + } + })), ] .into_iter() .flatten() diff --git a/src/config.rs b/src/config.rs index 22238a6..a64ccc8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,9 +6,8 @@ use iced::{ Color, Subscription, }; use inotify::{EventMask, Inotify, WatchMask}; -use log::warn; -use serde::{Deserialize, Deserializer}; -use std::{env, fs::File, path::Path}; +use serde::Deserialize; +use std::{any::TypeId, env, fs::File, path::Path}; use crate::app::Message; @@ -309,7 +308,7 @@ pub struct Config { pub clipboard_cmd: Option, #[serde(default = "default_truncate_title_after_length")] pub truncate_title_after_length: u32, - #[serde(deserialize_with = "try_default")] + #[serde(default)] pub updates: Option, #[serde(default)] pub system: SystemModuleConfig, @@ -323,21 +322,6 @@ pub struct Config { pub appearance: Appearance, } -fn try_default<'de, T, D>(deserializer: D) -> Result -where - T: Deserialize<'de> + Default + std::fmt::Debug, - D: Deserializer<'de>, -{ - // Try to deserialize the UpdatesModuleConfig - let result: Result = T::deserialize(deserializer); - - // If it fails, return None - result.or_else(|err| { - warn!("error deserializing: {:?}", err); - Ok(T::default()) - }) -} - fn default_log_level() -> String { "warn".to_owned() } @@ -379,8 +363,11 @@ pub fn read_config() -> Result { } pub fn subscription() -> Subscription { - Subscription::run(|| { - channel(100, move |mut output| async move { + let id = TypeId::of::(); + + Subscription::run_with_id( + id, + channel(100, |mut output| async move { let home_dir = env::var("HOME").expect("Could not get HOME environment variable"); let file_path = format!("{}{}", home_dir, CONFIG_PATH.replace('~', "")); @@ -419,6 +406,7 @@ pub fn subscription() -> Subscription { .expect("Failed to create event stream"); loop { + log::debug!("waiting for event"); let event = stream.next().await; match event { Some(Ok(inotify::Event { @@ -455,6 +443,8 @@ pub fn subscription() -> Subscription { } else { log::warn!("Failed to read config file: {:?}", new_config); } + + break; } Some(Ok(inotify::Event { mask: EventMask::DELETE, @@ -465,10 +455,12 @@ pub fn subscription() -> Subscription { break; } - _ => {} + other => { + log::debug!("other event {:?}", other); + } } } } - }) - }) + }), + ) } diff --git a/src/menu.rs b/src/menu.rs index c8e6cff..de8ba7b 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1,17 +1,20 @@ use crate::app::{self, MenuType}; use crate::config::Position; use iced::alignment::{Horizontal, Vertical}; -use iced::platform_specific::shell::commands::layer_surface::{set_layer, Layer}; +use iced::platform_specific::shell::commands::layer_surface::{ + set_keyboard_interactivity, set_layer, Layer, +}; use iced::widget::container::Style; use iced::widget::mouse_area; use iced::window::Id; use iced::{self, widget::container, Element, Task, Theme}; use iced::{Border, Length, Padding}; +use sctk::shell::wlr_layer::KeyboardInteractivity; #[derive(Debug, Clone)] pub struct Menu { - id: Id, - menu_type: Option, + pub id: Id, + pub menu_type: Option, } impl Menu { @@ -22,24 +25,29 @@ impl Menu { } } - pub fn open(&mut self, menu_type: MenuType) -> Task { - let task = set_layer(self.id, Layer::Overlay); - + pub fn open(&mut self, menu_type: MenuType) -> Task { self.menu_type.replace(menu_type); - task + Task::batch(vec![ + set_layer(self.id, Layer::Overlay), + set_keyboard_interactivity(self.id, KeyboardInteractivity::None), + ]) } pub fn close(&mut self) -> Task { if self.menu_type.is_some() { self.menu_type.take(); - set_layer(self.id, Layer::Background) + + Task::batch(vec![ + set_layer(self.id, Layer::Background), + set_keyboard_interactivity(self.id, KeyboardInteractivity::None), + ]) } else { Task::none() } } - pub fn toggle(&mut self, menu_type: MenuType) -> Task { + pub fn toggle(&mut self, menu_type: MenuType) -> Task { match self.menu_type.as_mut() { None => self.open(menu_type), Some(current) if *current == menu_type => self.close(), @@ -61,18 +69,6 @@ impl Menu { Task::none() } } - - pub fn is_menu(&self, id: Id) -> bool { - self.id == id - } - - pub fn get_menu_type_to_render(&self, id: Id) -> Option { - if self.id == id { - self.menu_type - } else { - None - } - } } pub enum MenuPosition { @@ -81,6 +77,7 @@ pub enum MenuPosition { } pub fn menu_wrapper( + id: Id, content: Element, position: MenuPosition, bar_position: Position, @@ -115,6 +112,6 @@ pub fn menu_wrapper( .width(Length::Fill) .height(Length::Fill), ) - .on_release(app::Message::CloseMenu) + .on_release(app::Message::CloseMenu(id)) .into() } diff --git a/src/modules/settings/audio.rs b/src/modules/settings/audio.rs index 17110fc..f3a61b1 100644 --- a/src/modules/settings/audio.rs +++ b/src/modules/settings/audio.rs @@ -9,6 +9,7 @@ use crate::{ }; use iced::{ widget::{button, column, container, horizontal_rule, row, slider, text, Column, Row}, + window::Id, Alignment, Element, Length, Theme, }; @@ -21,8 +22,8 @@ pub enum AudioMessage { SinkVolumeChanged(i32), ToggleSourceMute, SourceVolumeChanged(i32), - SinksMore, - SourcesMore, + SinksMore(Id), + SourcesMore(Id), } impl AudioData { @@ -87,7 +88,7 @@ impl AudioData { } } - pub fn sinks_submenu(&self, show_more: bool) -> Element { + pub fn sinks_submenu(&self, id: Id, show_more: bool) -> Element { audio_submenu( self.sinks .iter() @@ -104,14 +105,14 @@ impl AudioData { }) .collect(), if show_more { - Some(Message::Audio(AudioMessage::SinksMore)) + Some(Message::Audio(AudioMessage::SinksMore(id))) } else { None }, ) } - pub fn sources_submenu(&self, show_more: bool) -> Element { + pub fn sources_submenu(&self, id: Id, show_more: bool) -> Element { audio_submenu( self.sources .iter() @@ -128,7 +129,7 @@ impl AudioData { }) .collect(), if show_more { - Some(Message::Audio(AudioMessage::SourcesMore)) + Some(Message::Audio(AudioMessage::SourcesMore(id))) } else { None }, @@ -175,7 +176,7 @@ pub fn audio_slider<'a, Message: 'a + Clone>( .push( slider(0..=100, volume, volume_changed) .step(1) - .width(Length::Fill) + .width(Length::Fill), ) .push_maybe(with_submenu.map(|(submenu, msg)| { button(icon(match (slider_type, submenu) { diff --git a/src/modules/settings/bluetooth.rs b/src/modules/settings/bluetooth.rs index 18d2ff2..fcb1023 100644 --- a/src/modules/settings/bluetooth.rs +++ b/src/modules/settings/bluetooth.rs @@ -9,6 +9,7 @@ use crate::{ }; use iced::{ widget::{button, column, container, horizontal_rule, row, text, Column, Row}, + window::Id, Element, Length, Theme, }; @@ -16,12 +17,13 @@ use iced::{ pub enum BluetoothMessage { Event(ServiceEvent), Toggle, - More, + More(Id), } impl BluetoothData { pub fn get_quick_setting_button( &self, + id: Id, sub_menu: Option, show_more_button: bool, ) -> Option<(Element, Option>)> { @@ -41,11 +43,11 @@ impl BluetoothData { ), sub_menu .filter(|menu_type| *menu_type == SubMenu::Bluetooth) - .map(|_| sub_menu_wrapper(self.bluetooth_menu(show_more_button))), + .map(|_| sub_menu_wrapper(self.bluetooth_menu(id, show_more_button))), )) } - pub fn bluetooth_menu(&self, show_more_button: bool) -> Element { + pub fn bluetooth_menu(&self, id: Id, show_more_button: bool) -> Element { let main = if self.devices.is_empty() { text("No devices connected").into() } else { @@ -69,7 +71,7 @@ impl BluetoothData { main, horizontal_rule(1), button("More") - .on_press(Message::Bluetooth(BluetoothMessage::More)) + .on_press(Message::Bluetooth(BluetoothMessage::More(id))) .padding([4, 12]) .width(Length::Fill) .style(GhostButtonStyle.into_style()) diff --git a/src/modules/settings/mod.rs b/src/modules/settings/mod.rs index 883fe6e..6a01537 100644 --- a/src/modules/settings/mod.rs +++ b/src/modules/settings/mod.rs @@ -5,8 +5,8 @@ use crate::{ app::MenuType, components::icons::{icon, Icons}, config::SettingsModuleConfig, - menu::Menu, modules::settings::power::power_menu, + outputs::Outputs, password_dialog, services::{ audio::{AudioCommand, AudioService}, @@ -28,6 +28,7 @@ use iced::{ widget::{ button, column, container, horizontal_space, row, text, vertical_rule, Column, Row, Space, }, + window::Id, Alignment, Background, Border, Element, Length, Padding, Subscription, Task, Theme, }; use log::info; @@ -68,7 +69,7 @@ impl Default for Settings { #[derive(Debug, Clone)] pub enum Message { - ToggleMenu, + ToggleMenu(Id), UPower(UPowerMessage), Network(NetworkMessage), Bluetooth(BluetoothMessage), @@ -96,13 +97,13 @@ impl Settings { &mut self, message: Message, config: &SettingsModuleConfig, - menu: &mut Menu, + outputs: &mut Outputs, ) -> Task { match message { - Message::ToggleMenu => { + Message::ToggleMenu(id) => { self.sub_menu = None; self.password_dialog = None; - Task::batch(vec![menu.toggle(MenuType::Settings)]) + outputs.toggle_menu(id, MenuType::Settings) } Message::Audio(msg) => match msg { AudioMessage::Event(event) => match event { @@ -154,18 +155,18 @@ impl Settings { } Task::none() } - AudioMessage::SinksMore => { + AudioMessage::SinksMore(id) => { if let Some(cmd) = &config.audio_sinks_more_cmd { crate::utils::launcher::execute_command(cmd.to_string()); - menu.close() + outputs.close_menu(id) } else { Task::none() } } - AudioMessage::SourcesMore => { + AudioMessage::SourcesMore(id) => { if let Some(cmd) = &config.audio_sources_more_cmd { crate::utils::launcher::execute_command(cmd.to_string()); - menu.close() + outputs.close_menu(id) } else { Task::none() } @@ -272,18 +273,18 @@ impl Settings { Task::none() } } - NetworkMessage::WiFiMore => { + NetworkMessage::WiFiMore(id) => { if let Some(cmd) = &config.wifi_more_cmd { crate::utils::launcher::execute_command(cmd.to_string()); - menu.close() + outputs.close_menu(id) } else { Task::none() } } - NetworkMessage::VpnMore => { + NetworkMessage::VpnMore(id) => { if let Some(cmd) = &config.vpn_more_cmd { crate::utils::launcher::execute_command(cmd.to_string()); - menu.close() + outputs.close_menu(id) } else { Task::none() } @@ -327,10 +328,10 @@ impl Settings { Task::none() } } - BluetoothMessage::More => { + BluetoothMessage::More(id) => { if let Some(cmd) = &config.bluetooth_more_cmd { crate::utils::launcher::execute_command(cmd.to_string()); - menu.close() + outputs.close_menu(id) } else { Task::none() } @@ -451,7 +452,7 @@ impl Settings { } } - pub fn view(&self) -> Element { + pub fn view(&self, id: Id) -> Element { button( Row::new() .push_maybe( @@ -493,11 +494,11 @@ impl Settings { ) .style(HeaderButtonStyle::Right.into_style()) .padding([2, 8]) - .on_press(Message::ToggleMenu) + .on_press(Message::ToggleMenu(id)) .into() } - pub fn menu_view(&self, config: &SettingsModuleConfig) -> Element { + pub fn menu_view(&self, id: Id, config: &SettingsModuleConfig) -> Element { if let Some((ssid, current_password)) = &self.password_dialog { password_dialog::view(ssid, current_password).map(Message::PasswordDialog) } else { @@ -539,7 +540,7 @@ impl Settings { .unwrap_or((None, None)); let wifi_setting_button = self.network.as_ref().and_then(|n| { - n.get_wifi_quick_setting_button(self.sub_menu, config.wifi_more_cmd.is_some()) + n.get_wifi_quick_setting_button(id, self.sub_menu, config.wifi_more_cmd.is_some()) }); let quick_settings = quick_settings_section( vec![ @@ -549,12 +550,17 @@ impl Settings { .filter(|b| b.state != BluetoothState::Unavailable) .and_then(|b| { b.get_quick_setting_button( + id, self.sub_menu, config.bluetooth_more_cmd.is_some(), ) }), self.network.as_ref().map(|n| { - n.get_vpn_quick_setting_button(self.sub_menu, config.vpn_more_cmd.is_some()) + n.get_vpn_quick_setting_button( + id, + self.sub_menu, + config.vpn_more_cmd.is_some(), + ) }), self.network .as_ref() @@ -599,7 +605,7 @@ impl Settings { .and_then(|_| { self.audio.as_ref().map(|a| { sub_menu_wrapper( - a.sinks_submenu(config.audio_sinks_more_cmd.is_some()), + a.sinks_submenu(id, config.audio_sinks_more_cmd.is_some()), ) }) }), @@ -611,7 +617,7 @@ impl Settings { .and_then(|_| { self.audio.as_ref().map(|a| { sub_menu_wrapper( - a.sources_submenu(config.audio_sources_more_cmd.is_some()), + a.sources_submenu(id, config.audio_sources_more_cmd.is_some()), ) }) }), diff --git a/src/modules/settings/network.rs b/src/modules/settings/network.rs index bf72fb1..5d1008a 100644 --- a/src/modules/settings/network.rs +++ b/src/modules/settings/network.rs @@ -13,6 +13,7 @@ use crate::{ }; use iced::{ widget::{button, column, container, horizontal_rule, row, scrollable, text, toggler, Column}, + window::Id, Alignment, Element, Length, Theme, }; @@ -21,8 +22,8 @@ pub enum NetworkMessage { Event(ServiceEvent), ToggleWiFi, ScanNearByWiFi, - WiFiMore, - VpnMore, + WiFiMore(Id), + VpnMore(Id), SelectAccessPoint(AccessPoint), RequestWiFiPassword(String), ToggleVpn(Vpn), @@ -127,6 +128,7 @@ impl NetworkData { pub fn get_wifi_quick_setting_button( &self, + id: Id, sub_menu: Option, show_more_button: bool, ) -> Option<(Element, Option>)> { @@ -156,6 +158,7 @@ impl NetworkData { .filter(|menu_type| *menu_type == SubMenu::Wifi) .map(|_| { sub_menu_wrapper(self.wifi_menu( + id, active_connection.map(|(name, strengh, _)| (name.as_str(), *strengh)), show_more_button, )) @@ -169,6 +172,7 @@ impl NetworkData { pub fn get_vpn_quick_setting_button( &self, + id: Id, sub_menu: Option, show_more_button: bool, ) -> (Element, Option>) { @@ -185,12 +189,15 @@ impl NetworkData { ), sub_menu .filter(|menu_type| *menu_type == SubMenu::Vpn) - .map(|_| sub_menu_wrapper(self.vpn_menu(show_more_button)).map(Message::Network)), + .map(|_| { + sub_menu_wrapper(self.vpn_menu(id, show_more_button)).map(Message::Network) + }), ) } pub fn wifi_menu( &self, + id: Id, active_connection: Option<(&str, u8)>, show_more_button: bool, ) -> Element { @@ -281,7 +288,7 @@ impl NetworkData { main, horizontal_rule(1), button("More") - .on_press(NetworkMessage::WiFiMore) + .on_press(NetworkMessage::WiFiMore(id)) .padding([4, 12]) .width(Length::Fill) .style(GhostButtonStyle.into_style()), @@ -293,7 +300,7 @@ impl NetworkData { } } - pub fn vpn_menu(&self, show_more_button: bool) -> Element { + pub fn vpn_menu(&self, id: Id, show_more_button: bool) -> Element { let main = Column::with_children( self.known_connections .iter() @@ -323,7 +330,7 @@ impl NetworkData { main, horizontal_rule(1), button("More") - .on_press(NetworkMessage::VpnMore) + .on_press(NetworkMessage::VpnMore(id)) .padding([4, 12]) .width(Length::Fill) .style(GhostButtonStyle.into_style()), diff --git a/src/modules/updates.rs b/src/modules/updates.rs index 6eb5c34..a3ceceb 100644 --- a/src/modules/updates.rs +++ b/src/modules/updates.rs @@ -2,13 +2,14 @@ use crate::{ app::{self, MenuType}, components::icons::{icon, Icons}, config::UpdatesModuleConfig, - menu::Menu, + outputs::Outputs, style::{GhostButtonStyle, HeaderButtonStyle}, }; use iced::{ alignment::Horizontal, stream::channel, widget::{button, column, container, horizontal_rule, row, scrollable, text, Column}, + window::Id, Alignment, Element, Length, Padding, Subscription, Task, }; use log::error; @@ -70,12 +71,12 @@ async fn update(update_cmd: &str) { #[derive(Debug, Clone)] pub enum Message { - ToggleMenu, + ToggleMenu(Id), UpdatesCheckCompleted(Vec), UpdateFinished, ToggleUpdatesList, CheckNow, - Update, + Update(Id), } #[derive(Debug, Default, Clone, Eq, PartialEq)] @@ -97,7 +98,7 @@ impl Updates { &mut self, message: Message, config: &UpdatesModuleConfig, - menu: &mut Menu, + outputs: &mut Outputs, ) -> Task { match message { Message::UpdatesCheckCompleted(updates) => { @@ -106,9 +107,9 @@ impl Updates { Task::none() } - Message::ToggleMenu => { + Message::ToggleMenu(id) => { self.is_updates_list_open = false; - menu.toggle(MenuType::Updates) + outputs.toggle_menu(id, MenuType::Updates) } Message::UpdateFinished => { self.updates.clear(); @@ -129,7 +130,7 @@ impl Updates { move |updates| app::Message::Updates(Message::UpdatesCheckCompleted(updates)), ) } - Message::Update => { + Message::Update(id) => { let update_command = config.update_cmd.clone(); let mut cmds = vec![Task::perform( async move { @@ -143,14 +144,14 @@ impl Updates { move |_| app::Message::Updates(Message::UpdateFinished), )]; - cmds.push(menu.close_if(MenuType::Updates)); + cmds.push(outputs.close_menu_if(id, MenuType::Updates)); Task::batch(cmds) } } } - pub fn view(&self) -> Element { + pub fn view(&self, id: Id) -> Element { let mut content = row!(container(icon(match self.state { State::Checking => Icons::Refresh, State::Ready if self.updates.is_empty() => Icons::NoUpdatesAvailable, @@ -166,11 +167,11 @@ impl Updates { button(content) .padding([2, 7]) .style(HeaderButtonStyle::Full.into_style()) - .on_press(Message::ToggleMenu) + .on_press(Message::ToggleMenu(id)) .into() } - pub fn menu_view(&self) -> Element { + pub fn menu_view(&self, id: Id) -> Element { column!( if self.updates.is_empty() { convert::Into::>::into( @@ -237,7 +238,7 @@ impl Updates { button("Update") .style(GhostButtonStyle.into_style()) .padding([8, 8]) - .on_press(Message::Update) + .on_press(Message::Update(id)) .width(Length::Fill), button({ let mut content = row!(text("Check now").width(Length::Fill),); diff --git a/src/outputs.rs b/src/outputs.rs index 8a7e5c6..4257168 100644 --- a/src/outputs.rs +++ b/src/outputs.rs @@ -5,96 +5,124 @@ use iced::{ Task, }; use log::debug; -use sctk::{ - output::OutputInfo, - shell::wlr_layer::{Anchor, KeyboardInteractivity, Layer}, -}; +use sctk::shell::wlr_layer::{Anchor, KeyboardInteractivity, Layer}; use wayland_client::protocol::wl_output::WlOutput; -use crate::{config::Position, menu::Menu, HEIGHT}; +use crate::{app::MenuType, config::Position, menu::Menu, HEIGHT}; + +type ActiveOutput = (Id, Menu, Option<(String, WlOutput)>); #[derive(Debug, Default, Clone)] -pub struct Outputs(Vec<(Id, Menu, Option)>); +pub struct Outputs { + active: Vec, + inactive: Vec<(String, WlOutput)>, +} + +pub enum HasOutput { + Main, + Menu(Option), +} impl Outputs { - pub fn has(&self, id: Id) -> bool { - self.0.iter().any(|(layer_id, _)| *layer_id == id) + fn create_output_layers( + wl_output: WlOutput, + position: Position, + ) -> (Id, Id, Task) { + let id = Id::unique(); + let task = get_layer_surface(SctkLayerSurfaceSettings { + id, + size: Some((None, Some(HEIGHT))), + layer: Layer::Bottom, + pointer_interactivity: true, + keyboard_interactivity: KeyboardInteractivity::None, + exclusive_zone: HEIGHT as i32, + output: IcedOutput::Output(wl_output.clone()), + anchor: match position { + Position::Top => Anchor::TOP, + Position::Bottom => Anchor::BOTTOM, + } | Anchor::LEFT + | Anchor::RIGHT, + ..Default::default() + }); + + let menu_id = Id::unique(); + let menu_task = get_layer_surface(SctkLayerSurfaceSettings { + id: menu_id, + size: Some((None, None)), + layer: Layer::Background, + pointer_interactivity: true, + keyboard_interactivity: KeyboardInteractivity::None, + output: IcedOutput::Output(wl_output.clone()), + anchor: Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT, + ..Default::default() + }); + + (id, menu_id, Task::batch(vec![task, menu_task])) + } + + pub fn has(&self, id: Id) -> Option { + self.active.iter().find_map(|(layer_id, menu, _)| { + if *layer_id == id { + Some(HasOutput::Main) + } else if menu.id == id { + Some(HasOutput::Menu(menu.menu_type)) + } else { + None + } + }) } pub fn add( &mut self, request_outputs: &[String], position: Position, - output_info: Option, + name: &str, wl_output: WlOutput, ) -> Task { - let target = request_outputs.iter().any(|output| { - Some(output.as_str()) == output_info.as_ref().and_then(|info| info.name.as_deref()) - }); + let target = request_outputs.iter().any(|output| output.as_str() == name); + + if self.active.is_empty() { + debug!( + "No outputs, creating a new layer surface. Is a fallback surface {}", + target + ); - if self.0.is_empty() { - debug!("No outputs, creating a new layer surface. Is a fallback surface {}", target); - let id = Id::unique(); - let task = get_layer_surface(SctkLayerSurfaceSettings { - id, - size: Some((None, Some(HEIGHT))), - layer: Layer::Top, - pointer_interactivity: true, - keyboard_interactivity: KeyboardInteractivity::None, - exclusive_zone: HEIGHT as i32, - output: IcedOutput::Output(wl_output.clone()), - anchor: match position { - Position::Top => Anchor::TOP, - Position::Bottom => Anchor::BOTTOM, - } | Anchor::LEFT - | Anchor::RIGHT, - ..Default::default() - }); - - let menu_id = Id::unique(); - let menu = get_layer_surface(SctkLayerSurfaceSettings { - id: menu_id, - size: Some((None, None)), - layer: Layer::Background, - pointer_interactivity: true, - keyboard_interactivity: KeyboardInteractivity::OnDemand, - anchor: Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT, - ..Default::default() - }); - - self.0.push((id, target.then_some(wl_output))); + let (id, menu_id, task) = Self::create_output_layers(wl_output.clone(), position); + + self.active + .push((id, Menu::new(menu_id), Some((name.to_owned(), wl_output)))); task } else if target { debug!("Found target output, creating a new layer surface"); - let id = Id::unique(); - let create_task = get_layer_surface(SctkLayerSurfaceSettings { - id, - size: Some((None, Some(HEIGHT))), - pointer_interactivity: true, - keyboard_interactivity: KeyboardInteractivity::None, - exclusive_zone: HEIGHT as i32, - output: IcedOutput::Output(wl_output.clone()), - anchor: match position { - Position::Top => Anchor::TOP, - Position::Bottom => Anchor::BOTTOM, - } | Anchor::LEFT - | Anchor::RIGHT, - ..Default::default() - }); - - self.0.push((id, Some(wl_output))); - - if let Some(index) = self.0.iter().position(|(_, wl_output)| wl_output.is_none()) { + + let (id, menu_id, task) = Self::create_output_layers(wl_output.clone(), position); + + self.active + .push((id, Menu::new(menu_id), Some((name.to_owned(), wl_output)))); + + if let Some(index) = self + .active + .iter() + .position(|(_, _, wl_output)| wl_output.is_none()) + { debug!("Found fallback output, removing it"); - let (id, _) = self.0.swap_remove(index); - let destroy_task = destroy_layer_surface(id); - Task::batch(vec![create_task, destroy_task]) + let (id, menu, wl_output) = self.active.swap_remove(index); + let destroy_main_task = destroy_layer_surface(id); + let destroy_menu_task = destroy_layer_surface(menu.id); + + if let Some(wl_output) = wl_output { + self.inactive.push(wl_output); + } + + Task::batch(vec![task, destroy_main_task, destroy_menu_task]) } else { - create_task + task } } else { + self.inactive.push((name.to_owned(), wl_output)); + Task::none() } } @@ -104,42 +132,136 @@ impl Outputs { position: Position, wl_output: WlOutput, ) -> Task { - if let Some(to_remove) = self - .0 - .iter() - .position(|(_, output)| output.as_ref() == Some(&wl_output)) - { + if let Some(to_remove) = self.active.iter().position(|(_, _, output)| { + output.as_ref().map(|(_, wl_output)| wl_output) == Some(&wl_output) + }) { debug!("Removing layer surface for output"); - let (id, _) = self.0.swap_remove(to_remove); + let (id, menu, old_wl_output) = self.active.swap_remove(to_remove); + + let destroy_main_task = destroy_layer_surface(id); + let destroy_menu_task = destroy_layer_surface(menu.id); - let destroy_task = destroy_layer_surface(id); + if let Some(wl_output) = old_wl_output { + self.inactive.push(wl_output); + } - if self.0.is_empty() { + if self.active.is_empty() { debug!("No outputs left, creating a fallback layer surface"); - let id = Id::unique(); - let create_task = get_layer_surface(SctkLayerSurfaceSettings { - id, - size: Some((None, Some(HEIGHT))), - pointer_interactivity: true, - keyboard_interactivity: KeyboardInteractivity::None, - exclusive_zone: HEIGHT as i32, - output: IcedOutput::Active, - anchor: match position { - Position::Top => Anchor::TOP, - Position::Bottom => Anchor::BOTTOM, - } | Anchor::LEFT - | Anchor::RIGHT, - ..Default::default() - }); - - self.0.push((id, None)); - - Task::batch(vec![destroy_task, create_task]) + let (id, menu_id, task) = Self::create_output_layers(wl_output.clone(), position); + + self.active.push((id, Menu::new(menu_id), None)); + + Task::batch(vec![destroy_main_task, destroy_menu_task, task]) } else { - destroy_task + Task::batch(vec![destroy_main_task, destroy_menu_task]) } } else { Task::none() } } + + pub fn sync( + &mut self, + request_outputs: &[String], + position: Position, + ) -> Task { + debug!( + "Syncing outputs: {:?}, request_outputs: {:?}", + self, request_outputs + ); + + let to_remove = self + .active + .iter() + .filter_map(|(_, _, output)| { + if let Some((name, wl_output)) = output { + if !request_outputs.iter().any(|output| output.as_str() == name) { + Some(wl_output.clone()) + } else { + None + } + } else { + None + } + }) + .collect::>(); + debug!("Removing outputs: {:?}", to_remove); + + let to_add = self + .inactive + .iter() + .filter_map(|(name, wl_output)| { + if request_outputs.iter().any(|output| output.as_str() == name) { + Some((name.clone(), wl_output.clone())) + } else { + None + } + }) + .collect::>(); + debug!("Adding outputs: {:?}", to_add); + + let mut tasks = Vec::new(); + for wl_output in to_remove { + tasks.push(self.remove(position, wl_output)); + } + + for (name, wl_output) in to_add { + tasks.push(self.add(request_outputs, position, &name, wl_output)); + } + + Task::batch(tasks) + } + + pub fn toggle_menu(&mut self, id: Id, menu_type: MenuType) -> Task { + if let Some((_, menu, _)) = self + .active + .iter_mut() + .find(|(layer_id, menu, _)| *layer_id == id || menu.id == id) + { + let toggle_task = menu.toggle(menu_type); + let mut tasks = self + .active + .iter_mut() + .filter_map(|(layer_id, menu, _)| { + if *layer_id != id && menu.id != id { + Some(menu.close()) + } else { + None + } + }) + .collect::>(); + tasks.push(toggle_task); + Task::batch(tasks) + } else { + Task::none() + } + } + + pub fn close_menu(&mut self, id: Id) -> Task { + if let Some((_, menu, _)) = self + .active + .iter_mut() + .find(|(layer_id, menu, _)| *layer_id == id || menu.id == id) + { + menu.close() + } else { + Task::none() + } + } + + pub fn close_menu_if( + &mut self, + id: Id, + menu_type: MenuType, + ) -> Task { + if let Some((_, menu, _)) = self + .active + .iter_mut() + .find(|(layer_id, menu, _)| *layer_id == id || menu.id == id) + { + menu.close_if(menu_type) + } else { + Task::none() + } + } }