Confirm power and logout actions in centered system modals
This commit is contained in:
1 parent
4ca3a59cb7
commit
d8a23748c8
14 files changed
+673
-49
No files matched your search
@@ -864,7 +864,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
BindingAction::Close => self.close_focused_window(),
|
BindingAction::Close => self.close_focused_window(),
|
||||||
BindingAction::Exit => self.loop_signal.stop(),
|
BindingAction::Exit => self.request_logout_confirmation(),
|
||||||
BindingAction::Focus(direction) => self.focus_direction(direction),
|
BindingAction::Focus(direction) => self.focus_direction(direction),
|
||||||
BindingAction::Move(direction) => self.move_direction(direction),
|
BindingAction::Move(direction) => self.move_direction(direction),
|
||||||
BindingAction::Resize(direction) => self.resize_direction(direction),
|
BindingAction::Resize(direction) => self.resize_direction(direction),
|
||||||
|
|||||||
@@ -321,6 +321,7 @@ impl Ferese {
|
|||||||
fn dispatch_ipc_command(&mut self, command: &str, args: &Value) -> Result<Value, CommandError> {
|
fn dispatch_ipc_command(&mut self, command: &str, args: &Value) -> Result<Value, CommandError> {
|
||||||
match command {
|
match command {
|
||||||
"exit" => {} // The IPC worker stops the loop after writing the response.
|
"exit" => {} // The IPC worker stops the loop after writing the response.
|
||||||
|
"request-logout" => self.request_logout_confirmation(),
|
||||||
"reload-config" => self
|
"reload-config" => self
|
||||||
.reload_config()
|
.reload_config()
|
||||||
.map_err(|e| CommandError::new("invalid_config", e))?,
|
.map_err(|e| CommandError::new("invalid_config", e))?,
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ impl SessionLockHandler for Ferese {
|
|||||||
if self.session_lock.active {
|
if self.session_lock.active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
self.cancel_logout_confirmation();
|
||||||
self.session_lock.active = true;
|
self.session_lock.active = true;
|
||||||
self.session_lock.owner = Some(confirmation.ext_session_lock().clone());
|
self.session_lock.owner = Some(confirmation.ext_session_lock().clone());
|
||||||
self.session_lock.confirmation = Some(confirmation);
|
self.session_lock.confirmation = Some(confirmation);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use smithay::{
|
|||||||
use crate::{Ferese, private_client::ClientCapabilities, state::ClientState};
|
use crate::{Ferese, private_client::ClientCapabilities, state::ClientState};
|
||||||
|
|
||||||
pub(crate) fn init_global(display: &DisplayHandle) {
|
pub(crate) fn init_global(display: &DisplayHandle) {
|
||||||
display.create_global::<Ferese, FereseShellManagerV1, _>(2, ());
|
display.create_global::<Ferese, FereseShellManagerV1, _>(3, ());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn config_chunks(source: &str) -> Vec<&str> {
|
pub(crate) fn config_chunks(source: &str) -> Vec<&str> {
|
||||||
@@ -136,6 +136,21 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ferese_shell_v1::Request::ConfirmLogout { serial } => {
|
||||||
|
if state.logout_owner.as_ref() == Some(&shell.id())
|
||||||
|
&& consume_logout_confirmation(&mut state.pending_logout, serial)
|
||||||
|
{
|
||||||
|
state.logout_owner = None;
|
||||||
|
state.loop_signal.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ferese_shell_v1::Request::CancelLogout { serial } => {
|
||||||
|
if state.logout_owner.as_ref() == Some(&shell.id())
|
||||||
|
&& state.pending_logout == Some(serial)
|
||||||
|
{
|
||||||
|
state.cancel_logout_confirmation();
|
||||||
|
}
|
||||||
|
}
|
||||||
ferese_shell_v1::Request::EnterOverview => state.set_overview_active(true),
|
ferese_shell_v1::Request::EnterOverview => state.set_overview_active(true),
|
||||||
ferese_shell_v1::Request::ExitOverview => state.set_overview_active(false),
|
ferese_shell_v1::Request::ExitOverview => state.set_overview_active(false),
|
||||||
ferese_shell_v1::Request::SelectOverviewWindow {
|
ferese_shell_v1::Request::SelectOverviewWindow {
|
||||||
@@ -152,13 +167,78 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ferese_shell_v1::Request::Destroy => {}
|
ferese_shell_v1::Request::Destroy => {
|
||||||
|
if state.logout_owner.as_ref() == Some(&shell.id()) {
|
||||||
|
state.cancel_logout_confirmation();
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn destroyed(
|
||||||
|
state: &mut Self,
|
||||||
|
_client: smithay::reexports::wayland_server::backend::ClientId,
|
||||||
|
shell: &FereseShellV1,
|
||||||
|
_data: &(),
|
||||||
|
) {
|
||||||
|
if state.logout_owner.as_ref() == Some(&shell.id()) {
|
||||||
|
state.pending_logout = None;
|
||||||
|
state.logout_owner = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consume_logout_confirmation(pending: &mut Option<u32>, serial: u32) -> bool {
|
||||||
|
if *pending == Some(serial) {
|
||||||
|
*pending = None;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ferese {
|
impl Ferese {
|
||||||
|
pub(crate) fn request_logout_confirmation(&mut self) {
|
||||||
|
if self.session_lock.active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let shells = self
|
||||||
|
.shell_resources
|
||||||
|
.iter()
|
||||||
|
.filter_map(|shell| shell.upgrade().ok())
|
||||||
|
.filter(|shell| shell.version() >= 3)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if shells.is_empty() {
|
||||||
|
tracing::warn!("logout confirmation requires the updated Ferese shell");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.cancel_logout_confirmation();
|
||||||
|
let serial = u32::from(smithay::utils::SERIAL_COUNTER.next_serial());
|
||||||
|
let shell = &shells[0];
|
||||||
|
self.pending_logout = Some(serial);
|
||||||
|
self.logout_owner = Some(shell.id());
|
||||||
|
let output_name = self
|
||||||
|
.focused_output()
|
||||||
|
.map_or_else(String::new, |output| output.name());
|
||||||
|
shell.logout_requested(serial, output_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cancel_logout_confirmation(&mut self) {
|
||||||
|
if let Some(serial) = self.pending_logout.take() {
|
||||||
|
for shell in self
|
||||||
|
.shell_resources
|
||||||
|
.iter()
|
||||||
|
.filter_map(|shell| shell.upgrade().ok())
|
||||||
|
{
|
||||||
|
if self.logout_owner.as_ref() == Some(&shell.id()) && shell.version() >= 3 {
|
||||||
|
shell.logout_cancelled(serial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.logout_owner = None;
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn send_shell_snapshots(&mut self) {
|
pub(crate) fn send_shell_snapshots(&mut self) {
|
||||||
self.shell_resources
|
self.shell_resources
|
||||||
.retain(|resource| resource.upgrade().is_ok());
|
.retain(|resource| resource.upgrade().is_ok());
|
||||||
@@ -427,6 +507,16 @@ fn send_request_failed(shell: &FereseShellV1, request: ferese_shell_v1::FailedRe
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn logout_requires_the_current_confirmation_and_can_be_cancelled() {
|
||||||
|
let mut pending = Some(42);
|
||||||
|
assert!(!super::consume_logout_confirmation(&mut pending, 41));
|
||||||
|
assert_eq!(pending, Some(42));
|
||||||
|
assert!(super::consume_logout_confirmation(&mut pending, 42));
|
||||||
|
assert_eq!(pending, None);
|
||||||
|
assert!(!super::consume_logout_confirmation(&mut pending, 42));
|
||||||
|
}
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -257,6 +257,8 @@ pub struct Ferese {
|
|||||||
pub(crate) overview_scrims: HashMap<OutputId, crate::winit::OverviewScrim>,
|
pub(crate) overview_scrims: HashMap<OutputId, crate::winit::OverviewScrim>,
|
||||||
pub(crate) material_programs: HashMap<ErasedContextId, crate::winit::MaterialProgram>,
|
pub(crate) material_programs: HashMap<ErasedContextId, crate::winit::MaterialProgram>,
|
||||||
pub(crate) material_buffers: HashMap<WlSurface, crate::winit::MaterialBuffers>,
|
pub(crate) material_buffers: HashMap<WlSurface, crate::winit::MaterialBuffers>,
|
||||||
|
pub(crate) pending_logout: Option<u32>,
|
||||||
|
pub(crate) logout_owner: Option<smithay::reexports::wayland_server::backend::ObjectId>,
|
||||||
pub(crate) blur_programs: HashMap<ErasedContextId, crate::winit::BlurProgram>,
|
pub(crate) blur_programs: HashMap<ErasedContextId, crate::winit::BlurProgram>,
|
||||||
pub(crate) backdrop_generation: u64,
|
pub(crate) backdrop_generation: u64,
|
||||||
closing_windows: HashMap<WindowId, ClosingAnimation>,
|
closing_windows: HashMap<WindowId, ClosingAnimation>,
|
||||||
@@ -478,6 +480,8 @@ impl Ferese {
|
|||||||
overview_scrims: HashMap::new(),
|
overview_scrims: HashMap::new(),
|
||||||
material_programs: HashMap::new(),
|
material_programs: HashMap::new(),
|
||||||
material_buffers: HashMap::new(),
|
material_buffers: HashMap::new(),
|
||||||
|
pending_logout: None,
|
||||||
|
logout_owner: None,
|
||||||
blur_programs: HashMap::new(),
|
blur_programs: HashMap::new(),
|
||||||
backdrop_generation: 0,
|
backdrop_generation: 0,
|
||||||
closing_windows: HashMap::new(),
|
closing_windows: HashMap::new(),
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Backlog
|
||||||
|
|
||||||
|
- [ ] Keep overview open when selecting a workspace and show all of that workspace’s windows. Activate a window only after the user selects it.
|
||||||
|
|
||||||
|
- [ ] Add a first-run keybinding guide modal showing important keybindings and their functions. Keep showing it until the user disables the guide in Settings.
|
||||||
@@ -454,3 +454,5 @@ Settings → Windows. `theme.geometry.shell_radius` takes precedence over the ol
|
|||||||
`appearance.corner_radius` and then `theme.geometry.top_bar_radius` keys; when
|
`appearance.corner_radius` and then `theme.geometry.top_bar_radius` keys; when
|
||||||
none are set, the shell uses 14 px. Legacy clock/note radius fields no longer
|
none are set, the shell uses 14 px. Legacy clock/note radius fields no longer
|
||||||
override shell rounding.
|
override shell rounding.
|
||||||
|
|
||||||
|
Power actions use a centered system confirmation. The `exit` key binding (Super+Shift+E by default) requests logout confirmation; `feresectl request-logout` does the same. `feresectl exit` remains an immediate administrative exit.
|
||||||
@@ -89,7 +89,7 @@ fn parse_args(args: impl IntoIterator<Item = String>) -> Result<(String, Value),
|
|||||||
"toggle-floating" | "toggle-fullscreen" | "toggle-maximized" | "toggle-layout"
|
"toggle-floating" | "toggle-fullscreen" | "toggle-maximized" | "toggle-layout"
|
||||||
| "toggle-overview" | "cycle-column-width" | "center-column" | "consume" | "expel"
|
| "toggle-overview" | "cycle-column-width" | "center-column" | "consume" | "expel"
|
||||||
| "close" | "get-focused-window" | "get-workspaces" | "get-outputs" | "reload-config"
|
| "close" | "get-focused-window" | "get-workspaces" | "get-outputs" | "reload-config"
|
||||||
| "exit" => {
|
| "exit" | "request-logout" => {
|
||||||
if !positional.is_empty() {
|
if !positional.is_empty() {
|
||||||
return Err(format!("{command} does not accept arguments"));
|
return Err(format!("{command} does not accept arguments"));
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ fn socket_path() -> Result<PathBuf, io::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn usage() -> String {
|
fn usage() -> String {
|
||||||
"usage: feresectl screenshot [--geometry \"x,y WxH\"]\n feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl <toggle-floating|toggle-maximized|toggle-fullscreen|toggle-layout|toggle-overview>\n feresectl <cycle-column-width|center-column|consume|expel|close|exit>\n feresectl <get-focused-window|get-workspaces|get-outputs|reload-config>".to_owned()
|
"usage: feresectl screenshot [--geometry \"x,y WxH\"]\n feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl <toggle-floating|toggle-maximized|toggle-fullscreen|toggle-layout|toggle-overview>\n feresectl <cycle-column-width|center-column|consume|expel|close|exit|request-logout>\n feresectl <get-focused-window|get-workspaces|get-outputs|reload-config>".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
</copyright>
|
</copyright>
|
||||||
|
|
||||||
<interface name="ferese_shell_manager_v1" version="2">
|
<interface name="ferese_shell_manager_v1" version="3">
|
||||||
<description summary="create a capability-scoped Ferese shell controller">
|
<description summary="create a capability-scoped Ferese shell controller">
|
||||||
This private global is visible only to clients granted the shell-control
|
This private global is visible only to clients granted the shell-control
|
||||||
capability. It does not assign a Wayland surface role.
|
capability. It does not assign a Wayland surface role.
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<request name="destroy" type="destructor"/>
|
<request name="destroy" type="destructor"/>
|
||||||
</interface>
|
</interface>
|
||||||
|
|
||||||
<interface name="ferese_shell_v1" version="2">
|
<interface name="ferese_shell_v1" version="3">
|
||||||
<description summary="managed-window metadata and shell control">
|
<description summary="managed-window metadata and shell control">
|
||||||
Window, workspace, and output identifiers are unsigned 64-bit values
|
Window, workspace, and output identifiers are unsigned 64-bit values
|
||||||
encoded as high and low 32-bit words. Each snapshot atomically replaces
|
encoded as high and low 32-bit words. Each snapshot atomically replaces
|
||||||
@@ -143,5 +143,18 @@
|
|||||||
<arg name="source" type="string"/>
|
<arg name="source" type="string"/>
|
||||||
</event>
|
</event>
|
||||||
<event name="config_end" since="2"/>
|
<event name="config_end" since="2"/>
|
||||||
|
<request name="confirm_logout" since="3">
|
||||||
|
<arg name="serial" type="uint"/>
|
||||||
|
</request>
|
||||||
|
<request name="cancel_logout" since="3">
|
||||||
|
<arg name="serial" type="uint"/>
|
||||||
|
</request>
|
||||||
|
<event name="logout_cancelled" since="3">
|
||||||
|
<arg name="serial" type="uint"/>
|
||||||
|
</event>
|
||||||
|
<event name="logout_requested" since="3">
|
||||||
|
<arg name="serial" type="uint"/>
|
||||||
|
<arg name="output_name" type="string"/>
|
||||||
|
</event>
|
||||||
</interface>
|
</interface>
|
||||||
</protocol>
|
</protocol>
|
||||||
@@ -13,7 +13,7 @@ use ferese_protocols::shell::v1::client::{
|
|||||||
ferese_shell_v1::{self, FereseShellV1},
|
ferese_shell_v1::{self, FereseShellV1},
|
||||||
};
|
};
|
||||||
use wayland_client::{
|
use wayland_client::{
|
||||||
Connection, Dispatch, QueueHandle, WEnum, delegate_noop,
|
Connection, Dispatch, Proxy, QueueHandle, WEnum, delegate_noop,
|
||||||
globals::{GlobalListContents, registry_queue_init},
|
globals::{GlobalListContents, registry_queue_init},
|
||||||
protocol::wl_registry,
|
protocol::wl_registry,
|
||||||
};
|
};
|
||||||
@@ -63,6 +63,8 @@ pub(crate) struct ControlPoll {
|
|||||||
pub(crate) snapshot: Option<ShellSnapshot>,
|
pub(crate) snapshot: Option<ShellSnapshot>,
|
||||||
pub(crate) overview_active: Option<bool>,
|
pub(crate) overview_active: Option<bool>,
|
||||||
pub(crate) disconnected: bool,
|
pub(crate) disconnected: bool,
|
||||||
|
pub(crate) logout: Option<(u32, String)>,
|
||||||
|
pub(crate) logout_cancelled: Vec<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ControlUpdate {
|
enum ControlUpdate {
|
||||||
@@ -70,6 +72,8 @@ enum ControlUpdate {
|
|||||||
Snapshot(ShellSnapshot),
|
Snapshot(ShellSnapshot),
|
||||||
OverviewState(bool),
|
OverviewState(bool),
|
||||||
Disconnected,
|
Disconnected,
|
||||||
|
Logout(u32, String),
|
||||||
|
LogoutCancelled(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShellControl {
|
impl ShellControl {
|
||||||
@@ -77,7 +81,7 @@ impl ShellControl {
|
|||||||
let connection = control_connection()?;
|
let connection = control_connection()?;
|
||||||
let (globals, mut queue) = registry_queue_init::<ControlState>(&connection)?;
|
let (globals, mut queue) = registry_queue_init::<ControlState>(&connection)?;
|
||||||
let qh = queue.handle();
|
let qh = queue.handle();
|
||||||
let manager = globals.bind::<FereseShellManagerV1, _, _>(&qh, 1..=2, ())?;
|
let manager = globals.bind::<FereseShellManagerV1, _, _>(&qh, 1..=3, ())?;
|
||||||
let shell = manager.get_shell(&qh, ());
|
let shell = manager.get_shell(&qh, ());
|
||||||
let (sender, updates) = mpsc::channel();
|
let (sender, updates) = mpsc::channel();
|
||||||
let mut state = ControlState::new(sender);
|
let mut state = ControlState::new(sender);
|
||||||
@@ -104,10 +108,23 @@ impl ShellControl {
|
|||||||
snapshot: None,
|
snapshot: None,
|
||||||
overview_active: None,
|
overview_active: None,
|
||||||
disconnected: false,
|
disconnected: false,
|
||||||
|
logout: None,
|
||||||
|
logout_cancelled: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match self.updates.try_recv() {
|
match self.updates.try_recv() {
|
||||||
|
Ok(ControlUpdate::Logout(serial, output)) => poll.logout = Some((serial, output)),
|
||||||
|
Ok(ControlUpdate::LogoutCancelled(serial)) => {
|
||||||
|
if poll
|
||||||
|
.logout
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|(pending, _)| *pending == serial)
|
||||||
|
{
|
||||||
|
poll.logout = None;
|
||||||
|
}
|
||||||
|
poll.logout_cancelled.push(serial);
|
||||||
|
}
|
||||||
Ok(ControlUpdate::Config(source)) => poll.config = Some(source),
|
Ok(ControlUpdate::Config(source)) => poll.config = Some(source),
|
||||||
Ok(ControlUpdate::Snapshot(snapshot)) => poll.snapshot = Some(snapshot),
|
Ok(ControlUpdate::Snapshot(snapshot)) => poll.snapshot = Some(snapshot),
|
||||||
Ok(ControlUpdate::OverviewState(active)) => poll.overview_active = Some(active),
|
Ok(ControlUpdate::OverviewState(active)) => poll.overview_active = Some(active),
|
||||||
@@ -133,6 +150,20 @@ impl ShellControl {
|
|||||||
let _ = self.connection.flush();
|
let _ = self.connection.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn confirm_logout(&self, serial: u32) {
|
||||||
|
if self.shell.version() >= 3 {
|
||||||
|
self.shell.confirm_logout(serial);
|
||||||
|
let _ = self.connection.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cancel_logout(&self, serial: u32) {
|
||||||
|
if self.shell.version() >= 3 {
|
||||||
|
self.shell.cancel_logout(serial);
|
||||||
|
let _ = self.connection.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_overview_active(&self, active: bool) {
|
pub(crate) fn set_overview_active(&self, active: bool) {
|
||||||
if active {
|
if active {
|
||||||
self.shell.enter_overview();
|
self.shell.enter_overview();
|
||||||
@@ -282,6 +313,17 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
|||||||
.sender
|
.sender
|
||||||
.send(ControlUpdate::Snapshot(state.pending.clone()));
|
.send(ControlUpdate::Snapshot(state.pending.clone()));
|
||||||
}
|
}
|
||||||
|
ferese_shell_v1::Event::LogoutRequested {
|
||||||
|
serial,
|
||||||
|
output_name,
|
||||||
|
} => {
|
||||||
|
let _ = state
|
||||||
|
.sender
|
||||||
|
.send(ControlUpdate::Logout(serial, output_name));
|
||||||
|
}
|
||||||
|
ferese_shell_v1::Event::LogoutCancelled { serial } => {
|
||||||
|
let _ = state.sender.send(ControlUpdate::LogoutCancelled(serial));
|
||||||
|
}
|
||||||
ferese_shell_v1::Event::OverviewState { active } => {
|
ferese_shell_v1::Event::OverviewState { active } => {
|
||||||
let _ = state.sender.send(ControlUpdate::OverviewState(active != 0));
|
let _ = state.sender.send(ControlUpdate::OverviewState(active != 0));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod notifications;
|
|||||||
mod recording;
|
mod recording;
|
||||||
mod status;
|
mod status;
|
||||||
mod status_ui;
|
mod status_ui;
|
||||||
|
mod system_modal;
|
||||||
|
|
||||||
use cosmic::Element;
|
use cosmic::Element;
|
||||||
use cosmic::app::{Core, Settings, Task};
|
use cosmic::app::{Core, Settings, Task};
|
||||||
@@ -174,6 +175,8 @@ struct FereseShell {
|
|||||||
calendar_offset: i32,
|
calendar_offset: i32,
|
||||||
status_error: Option<String>,
|
status_error: Option<String>,
|
||||||
menu: Option<status_ui::OpenMenu>,
|
menu: Option<status_ui::OpenMenu>,
|
||||||
|
power_modal: Option<system_modal::PowerModal>,
|
||||||
|
pending_power: Option<(window::Id, system_modal::PowerAction)>,
|
||||||
note_editor: Option<DesktopNoteEditor>,
|
note_editor: Option<DesktopNoteEditor>,
|
||||||
note_drag: Option<NoteDrag>,
|
note_drag: Option<NoteDrag>,
|
||||||
note_pointer: std::collections::HashMap<window::Id, cosmic::iced::Point>,
|
note_pointer: std::collections::HashMap<window::Id, cosmic::iced::Point>,
|
||||||
@@ -249,6 +252,9 @@ enum Message {
|
|||||||
Control(status::Action),
|
Control(status::Action),
|
||||||
ConfirmPower(status::Action),
|
ConfirmPower(status::Action),
|
||||||
CancelPower,
|
CancelPower,
|
||||||
|
ExecutePower,
|
||||||
|
AnimatePower,
|
||||||
|
PowerCompleted(window::Id, Result<(), String>),
|
||||||
CalendarMonth(i32),
|
CalendarMonth(i32),
|
||||||
CalendarToday,
|
CalendarToday,
|
||||||
}
|
}
|
||||||
@@ -286,6 +292,8 @@ impl cosmic::Application for FereseShell {
|
|||||||
calendar_offset: 0,
|
calendar_offset: 0,
|
||||||
status_error: None,
|
status_error: None,
|
||||||
menu: None,
|
menu: None,
|
||||||
|
power_modal: None,
|
||||||
|
pending_power: None,
|
||||||
note_editor: None,
|
note_editor: None,
|
||||||
note_drag: None,
|
note_drag: None,
|
||||||
note_pointer: Default::default(),
|
note_pointer: Default::default(),
|
||||||
@@ -322,6 +330,15 @@ impl cosmic::Application for FereseShell {
|
|||||||
|
|
||||||
fn subscription(&self) -> Subscription<Self::Message> {
|
fn subscription(&self) -> Subscription<Self::Message> {
|
||||||
Subscription::batch([
|
Subscription::batch([
|
||||||
|
if self
|
||||||
|
.power_modal
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|modal| modal.motion.animating() || modal.motion.closing())
|
||||||
|
{
|
||||||
|
cosmic::iced::time::every(Duration::from_millis(16)).map(|_| Message::AnimatePower)
|
||||||
|
} else {
|
||||||
|
Subscription::none()
|
||||||
|
},
|
||||||
self.notifications
|
self.notifications
|
||||||
.subscription()
|
.subscription()
|
||||||
.map(Message::NotificationEvent),
|
.map(Message::NotificationEvent),
|
||||||
@@ -407,6 +424,8 @@ impl cosmic::Application for FereseShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.update_power_materials();
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
Message::BeginNoteEdit(id) => {
|
Message::BeginNoteEdit(id) => {
|
||||||
let mut tasks = vec![self.finish_note_edit()];
|
let mut tasks = vec![self.finish_note_edit()];
|
||||||
@@ -490,6 +509,7 @@ impl cosmic::Application for FereseShell {
|
|||||||
self.attach_effects(id, &surface)
|
self.attach_effects(id, &surface)
|
||||||
}
|
}
|
||||||
Ok((_connection, surface)) => {
|
Ok((_connection, surface)) => {
|
||||||
|
self.attach_power_material(id, &surface);
|
||||||
if let Some(entry) = &mut self.notification_surface
|
if let Some(entry) = &mut self.notification_surface
|
||||||
&& entry.id == id
|
&& entry.id == id
|
||||||
&& entry.effects.is_none()
|
&& entry.effects.is_none()
|
||||||
@@ -609,11 +629,14 @@ impl cosmic::Application for FereseShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::ConfirmPower(action) => {
|
Message::ConfirmPower(action) => {
|
||||||
if let Some(menu) = &mut self.menu {
|
if let Some(action) = system_modal::PowerAction::from_status(action) {
|
||||||
menu.confirm = Some(action);
|
return self.open_power_modal(action, None);
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::ExecutePower => self.execute_power_modal(),
|
||||||
|
Message::AnimatePower => self.animate_power_modal(),
|
||||||
|
Message::PowerCompleted(id, result) => self.finish_power_action(id, result),
|
||||||
Message::CalendarMonth(delta) => {
|
Message::CalendarMonth(delta) => {
|
||||||
self.calendar_offset = (self.calendar_offset + delta).clamp(-1200, 1200);
|
self.calendar_offset = (self.calendar_offset + delta).clamp(-1200, 1200);
|
||||||
Task::none()
|
Task::none()
|
||||||
@@ -622,13 +645,12 @@ impl cosmic::Application for FereseShell {
|
|||||||
self.calendar_offset = 0;
|
self.calendar_offset = 0;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::CancelPower => {
|
Message::CancelPower => self.close_power_modal(),
|
||||||
if let Some(menu) = &mut self.menu {
|
|
||||||
menu.confirm = None;
|
|
||||||
}
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
Message::Control(action) => {
|
Message::Control(action) => {
|
||||||
|
if let Some(action) = system_modal::PowerAction::from_status(action.clone()) {
|
||||||
|
return self.open_power_modal(action, None);
|
||||||
|
}
|
||||||
|
|
||||||
if let status::Action::PowerProfile(profile) = action {
|
if let status::Action::PowerProfile(profile) = action {
|
||||||
self.status_error = self
|
self.status_error = self
|
||||||
.status_service
|
.status_service
|
||||||
@@ -738,7 +760,19 @@ impl cosmic::Application for FereseShell {
|
|||||||
if self.bar_hidden(self.bar_surface_id) {
|
if self.bar_hidden(self.bar_surface_id) {
|
||||||
tasks.push(self.destroy_menu());
|
tasks.push(self.destroy_menu());
|
||||||
}
|
}
|
||||||
return Task::batch(tasks);
|
reload_task = Task::batch(tasks);
|
||||||
|
}
|
||||||
|
for serial in poll.logout_cancelled {
|
||||||
|
reload_task = Task::batch([reload_task, self.cancel_logout_modal(serial)]);
|
||||||
|
}
|
||||||
|
if let Some((serial, output)) = poll.logout {
|
||||||
|
reload_task = Task::batch([
|
||||||
|
reload_task,
|
||||||
|
self.open_power_modal(
|
||||||
|
system_modal::PowerAction::Logout(serial),
|
||||||
|
Some(&output),
|
||||||
|
),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reload_task
|
reload_task
|
||||||
@@ -904,7 +938,11 @@ impl FereseShell {
|
|||||||
Task::none()
|
Task::none()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut tasks = vec![destroy_layer_surface(entry.bar), menu];
|
let mut tasks = vec![
|
||||||
|
destroy_layer_surface(entry.bar),
|
||||||
|
menu,
|
||||||
|
self.destroy_power_modal(true),
|
||||||
|
];
|
||||||
if self.note_drag.as_ref().is_some_and(|drag| {
|
if self.note_drag.as_ref().is_some_and(|drag| {
|
||||||
entry.clock == Some(drag.source)
|
entry.clock == Some(drag.source)
|
||||||
|| entry.notes.iter().any(|(_, id)| *id == drag.source)
|
|| entry.notes.iter().any(|(_, id)| *id == drag.source)
|
||||||
@@ -1046,6 +1084,7 @@ impl FereseShell {
|
|||||||
Task::batch(tasks),
|
Task::batch(tasks),
|
||||||
self.rebuild_clocks(false),
|
self.rebuild_clocks(false),
|
||||||
self.rebuild_notes(false),
|
self.rebuild_notes(false),
|
||||||
|
self.destroy_power_modal(true),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1836,6 +1875,9 @@ impl FereseShell {
|
|||||||
..
|
..
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
|
if self.power_modal.is_some() {
|
||||||
|
return self.close_power_modal();
|
||||||
|
}
|
||||||
if self.note_drag.is_some() {
|
if self.note_drag.is_some() {
|
||||||
return self.finish_note_drag(false);
|
return self.finish_note_drag(false);
|
||||||
}
|
}
|
||||||
@@ -1857,6 +1899,10 @@ impl FereseShell {
|
|||||||
Event::Window(window::Event::Opened { .. })
|
Event::Window(window::Event::Opened { .. })
|
||||||
if self.outputs.iter().any(|entry| entry.bar == id)
|
if self.outputs.iter().any(|entry| entry.bar == id)
|
||||||
|| self.menu.as_ref().is_some_and(|menu| menu.id == id)
|
|| self.menu.as_ref().is_some_and(|menu| menu.id == id)
|
||||||
|
|| self
|
||||||
|
.power_modal
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|modal| modal.contains(id))
|
||||||
|| self
|
|| self
|
||||||
.notification_surface
|
.notification_surface
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -1944,6 +1990,14 @@ impl FereseShell {
|
|||||||
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Event::Window(window::Event::Closed)
|
||||||
|
if self
|
||||||
|
.power_modal
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|modal| modal.contains(id)) =>
|
||||||
|
{
|
||||||
|
self.close_power_modal()
|
||||||
|
}
|
||||||
Event::Window(window::Event::Closed) => Task::none(),
|
Event::Window(window::Event::Closed) => Task::none(),
|
||||||
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(
|
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(
|
||||||
_,
|
_,
|
||||||
@@ -2002,6 +2056,7 @@ impl FereseShell {
|
|||||||
layer_id,
|
layer_id,
|
||||||
))) if self.outputs.iter().any(|entry| entry.bar == layer_id) => {
|
))) if self.outputs.iter().any(|entry| entry.bar == layer_id) => {
|
||||||
self.attach_effects(layer_id, &surface);
|
self.attach_effects(layer_id, &surface);
|
||||||
|
self.attach_power_material(layer_id, &surface);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
_ => Task::none(),
|
_ => Task::none(),
|
||||||
@@ -2644,6 +2699,14 @@ impl EffectsBinding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_regions(&self, regions: &[[i32; 5]]) -> Result<(), Box<dyn std::error::Error>> {
|
fn set_regions(&self, regions: &[[i32; 5]]) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.set_material_regions(regions, ferese_surface_effects_v1::Role::Popover)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_material_regions(
|
||||||
|
&self,
|
||||||
|
regions: &[[i32; 5]],
|
||||||
|
role: ferese_surface_effects_v1::Role,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if self.surface.version() < 2 || self.regions.borrow().as_deref() == Some(regions) {
|
if self.surface.version() < 2 || self.regions.borrow().as_deref() == Some(regions) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -2656,8 +2719,7 @@ impl EffectsBinding {
|
|||||||
if regions.is_empty() {
|
if regions.is_empty() {
|
||||||
self.surface.clear_role();
|
self.surface.clear_role();
|
||||||
} else {
|
} else {
|
||||||
self.surface
|
self.surface.set_role(role);
|
||||||
.set_role(ferese_surface_effects_v1::Role::Popover);
|
|
||||||
}
|
}
|
||||||
self.connection.flush()?;
|
self.connection.flush()?;
|
||||||
*self.regions.borrow_mut() = Some(regions.to_vec());
|
*self.regions.borrow_mut() = Some(regions.to_vec());
|
||||||
|
|||||||
@@ -254,13 +254,17 @@ pub fn available(program: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run(program: &str, args: &[&str]) -> Result<String, String> {
|
fn run(program: &str, args: &[&str]) -> Result<String, String> {
|
||||||
|
run_with_timeout(program, args, "2s")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_with_timeout(program: &str, args: &[&str], timeout: &str) -> Result<String, String> {
|
||||||
if !available(program) {
|
if !available(program) {
|
||||||
return Err(format!("{program} is not installed"));
|
return Err(format!("{program} is not installed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coreutils timeout bounds disconnected D-Bus services, too. Never invoke a shell.
|
// Coreutils timeout bounds disconnected D-Bus services, too. Never invoke a shell.
|
||||||
let output = Command::new("timeout")
|
let output = Command::new("timeout")
|
||||||
.args(["--kill-after=1s", "2s", program])
|
.args(["--kill-after=1s", timeout, program])
|
||||||
.args(args)
|
.args(args)
|
||||||
.env("LC_ALL", "C")
|
.env("LC_ALL", "C")
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
@@ -519,6 +523,16 @@ fn notifications(bus: &mut StatusBus) -> Option<Notifications> {
|
|||||||
Some(Notifications { count, dnd })
|
Some(Notifications { count, dnd })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn execute_power(action: Action) -> Result<(), String> {
|
||||||
|
let action = match action {
|
||||||
|
Action::Poweroff => "poweroff",
|
||||||
|
Action::Reboot => "reboot",
|
||||||
|
Action::Suspend => "suspend",
|
||||||
|
_ => return Err("Invalid power action".to_owned()),
|
||||||
|
};
|
||||||
|
run_with_timeout("systemctl", &[action], "120s").map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
fn execute(action: &Action, settings: Option<&[String]>) -> Result<(), String> {
|
fn execute(action: &Action, settings: Option<&[String]>) -> Result<(), String> {
|
||||||
match action {
|
match action {
|
||||||
Action::Wifi(on) => run("nmcli", &["radio", "wifi", if *on { "on" } else { "off" }]),
|
Action::Wifi(on) => run("nmcli", &["radio", "wifi", if *on { "on" } else { "off" }]),
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ pub struct OpenMenu {
|
|||||||
pub id: window::Id,
|
pub id: window::Id,
|
||||||
pub kind: Menu,
|
pub kind: Menu,
|
||||||
pub motion: super::motion::PopupMotion,
|
pub motion: super::motion::PopupMotion,
|
||||||
pub confirm: Option<Action>,
|
|
||||||
pub effects: Option<EffectsBinding>,
|
pub effects: Option<EffectsBinding>,
|
||||||
pub regions: super::motion::Regions,
|
pub regions: super::motion::Regions,
|
||||||
}
|
}
|
||||||
@@ -118,7 +117,6 @@ impl FereseShell {
|
|||||||
id,
|
id,
|
||||||
kind,
|
kind,
|
||||||
motion: super::motion::PopupMotion::new(self.config.animations),
|
motion: super::motion::PopupMotion::new(self.config.animations),
|
||||||
confirm: None,
|
|
||||||
effects: None,
|
effects: None,
|
||||||
regions: Default::default(),
|
regions: Default::default(),
|
||||||
});
|
});
|
||||||
@@ -456,30 +454,7 @@ impl FereseShell {
|
|||||||
if kind != Menu::Calendar {
|
if kind != Menu::Calendar {
|
||||||
rows = rows.push(heading);
|
rows = rows.push(heading);
|
||||||
}
|
}
|
||||||
if let Some(action) = &menu.confirm {
|
if kind == Menu::Calendar {
|
||||||
let title = match action {
|
|
||||||
Action::Poweroff => "Power off this computer?",
|
|
||||||
Action::Suspend => "Suspend this computer?",
|
|
||||||
_ => "Restart this computer?",
|
|
||||||
};
|
|
||||||
rows = rows
|
|
||||||
.push(text(title))
|
|
||||||
.push(
|
|
||||||
text(if matches!(action, Action::Suspend) {
|
|
||||||
"The session will not be locked. Anyone can access it after waking."
|
|
||||||
} else {
|
|
||||||
"Save your work before continuing."
|
|
||||||
})
|
|
||||||
.size(13),
|
|
||||||
)
|
|
||||||
.push(
|
|
||||||
row![
|
|
||||||
menu_button("Cancel", Message::CancelPower, primary, p),
|
|
||||||
menu_button("Confirm", Message::Control(action.clone()), primary, p)
|
|
||||||
]
|
|
||||||
.spacing(8),
|
|
||||||
);
|
|
||||||
} else if kind == Menu::Calendar {
|
|
||||||
rows = rows.push(calendar_grid(self.calendar_offset, theme, p));
|
rows = rows.push(calendar_grid(self.calendar_offset, theme, p));
|
||||||
} else {
|
} else {
|
||||||
let combined = menu.kind == Menu::System;
|
let combined = menu.kind == Menu::System;
|
||||||
@@ -1397,7 +1372,6 @@ mod tests {
|
|||||||
id: window::Id::unique(),
|
id: window::Id::unique(),
|
||||||
kind: Menu::Network,
|
kind: Menu::Network,
|
||||||
motion,
|
motion,
|
||||||
confirm: None,
|
|
||||||
effects: None,
|
effects: None,
|
||||||
regions: Default::default(),
|
regions: Default::default(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
use super::*;
|
||||||
|
use cosmic::iced::widget::Space;
|
||||||
|
use cosmic::widget::{column, text};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(super) enum PowerAction {
|
||||||
|
Poweroff,
|
||||||
|
Reboot,
|
||||||
|
Suspend,
|
||||||
|
Logout(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PowerAction {
|
||||||
|
pub(super) fn from_status(action: status::Action) -> Option<Self> {
|
||||||
|
match action {
|
||||||
|
status::Action::Poweroff => Some(Self::Poweroff),
|
||||||
|
status::Action::Reboot => Some(Self::Reboot),
|
||||||
|
status::Action::Suspend => Some(Self::Suspend),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Poweroff => "Power off",
|
||||||
|
Self::Reboot => "Restart",
|
||||||
|
Self::Suspend => "Suspend",
|
||||||
|
Self::Logout(_) => "Log out",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Poweroff => "Power off this computer?",
|
||||||
|
Self::Reboot => "Restart this computer?",
|
||||||
|
Self::Suspend => "Suspend this computer?",
|
||||||
|
Self::Logout(_) => "Log out of Ferese?",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Suspend => "Your apps will stay open. The session will not be locked.",
|
||||||
|
Self::Logout(_) => "Save your work. Your apps will close when you log out.",
|
||||||
|
_ => "Save your work before continuing.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ModalSurface {
|
||||||
|
id: window::Id,
|
||||||
|
primary: bool,
|
||||||
|
effects: Option<EffectsBinding>,
|
||||||
|
regions: motion::Regions,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct PowerModal {
|
||||||
|
action: PowerAction,
|
||||||
|
surfaces: Vec<ModalSurface>,
|
||||||
|
pub(super) motion: motion::PopupMotion,
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PowerModal {
|
||||||
|
pub(super) fn contains(&self, id: window::Id) -> bool {
|
||||||
|
self.surfaces.iter().any(|surface| surface.id == id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FereseShell {
|
||||||
|
pub(super) fn open_power_modal(
|
||||||
|
&mut self,
|
||||||
|
action: PowerAction,
|
||||||
|
output_name: Option<&str>,
|
||||||
|
) -> Task<Message> {
|
||||||
|
if self.pending_power.is_some() || self.outputs.is_empty() {
|
||||||
|
if let PowerAction::Logout(serial) = action
|
||||||
|
&& let Some(control) = &self.control
|
||||||
|
{
|
||||||
|
control.cancel_logout(serial);
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.power_modal
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|modal| modal.action == action && !modal.motion.closing())
|
||||||
|
{
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let primary = output_name
|
||||||
|
.and_then(|name| {
|
||||||
|
self.outputs
|
||||||
|
.iter()
|
||||||
|
.position(|output| output.name.as_deref() == Some(name))
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
self.outputs
|
||||||
|
.iter()
|
||||||
|
.position(|output| output.bar == self.bar_surface_id)
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
let mut tasks = vec![
|
||||||
|
self.destroy_power_modal(true),
|
||||||
|
self.destroy_menu(),
|
||||||
|
self.close_notification_history(),
|
||||||
|
];
|
||||||
|
let mut surfaces = Vec::new();
|
||||||
|
let mut outputs = (0..self.outputs.len()).collect::<Vec<_>>();
|
||||||
|
outputs.sort_by_key(|index| *index == primary);
|
||||||
|
for index in outputs {
|
||||||
|
let id = window::Id::unique();
|
||||||
|
let output = self.outputs[index].output.clone();
|
||||||
|
let primary = index == primary;
|
||||||
|
surfaces.push(ModalSurface {
|
||||||
|
id,
|
||||||
|
primary,
|
||||||
|
effects: None,
|
||||||
|
regions: Default::default(),
|
||||||
|
});
|
||||||
|
let action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||||
|
|_| Default::default(),
|
||||||
|
move |_| SctkLayerSurfaceSettings {
|
||||||
|
id,
|
||||||
|
layer: Layer::Overlay,
|
||||||
|
keyboard_interactivity: if primary {
|
||||||
|
KeyboardInteractivity::Exclusive
|
||||||
|
} else {
|
||||||
|
KeyboardInteractivity::None
|
||||||
|
},
|
||||||
|
anchor: Anchor::TOP | Anchor::RIGHT | Anchor::BOTTOM | Anchor::LEFT,
|
||||||
|
output: IcedOutput::Output(output.clone()),
|
||||||
|
namespace: "ferese-system-modal".to_owned(),
|
||||||
|
exclusive_zone: -1,
|
||||||
|
size: Some((None, None)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(Box::new(move |app| app.view_power_modal(id))),
|
||||||
|
);
|
||||||
|
tasks.push(cosmic::task::message(cosmic::Action::Surface(action)));
|
||||||
|
}
|
||||||
|
self.power_modal = Some(PowerModal {
|
||||||
|
action,
|
||||||
|
surfaces,
|
||||||
|
motion: motion::PopupMotion::new(self.config.animations),
|
||||||
|
error: None,
|
||||||
|
});
|
||||||
|
Task::batch(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn attach_power_material(
|
||||||
|
&mut self,
|
||||||
|
id: window::Id,
|
||||||
|
surface: &wl_surface::WlSurface,
|
||||||
|
) {
|
||||||
|
let Some(modal) = &mut self.power_modal else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(entry) = modal.surfaces.iter_mut().find(|entry| entry.id == id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
modal.motion.begin(Instant::now());
|
||||||
|
if entry.primary && entry.effects.is_none() {
|
||||||
|
match EffectsBinding::attach_role(surface, None, 0.0) {
|
||||||
|
Ok(effects) => entry.effects = Some(effects),
|
||||||
|
Err(error) => eprintln!("ferese-shell: system modal material unavailable: {error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn update_power_materials(&self) {
|
||||||
|
if let Some(modal) = &self.power_modal {
|
||||||
|
for entry in &modal.surfaces {
|
||||||
|
if let Some(effects) = &entry.effects {
|
||||||
|
let regions = entry.regions.lock().unwrap().clone();
|
||||||
|
let _ = effects
|
||||||
|
.set_material_regions(®ions, ferese_surface_effects_v1::Role::Modal);
|
||||||
|
let _ = effects.set_opacity(modal.motion.progress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn close_power_modal(&mut self) -> Task<Message> {
|
||||||
|
if let Some(modal) = &mut self.power_modal {
|
||||||
|
if let PowerAction::Logout(serial) = modal.action
|
||||||
|
&& let Some(control) = &self.control
|
||||||
|
{
|
||||||
|
control.cancel_logout(serial);
|
||||||
|
}
|
||||||
|
modal.motion.retarget(0.0, Instant::now());
|
||||||
|
}
|
||||||
|
self.animate_power_modal()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn animate_power_modal(&mut self) -> Task<Message> {
|
||||||
|
if self
|
||||||
|
.power_modal
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|modal| modal.motion.closing() && !modal.motion.animating())
|
||||||
|
{
|
||||||
|
return self.destroy_power_modal(false);
|
||||||
|
}
|
||||||
|
self.update_power_materials();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn destroy_power_modal(&mut self, cancel: bool) -> Task<Message> {
|
||||||
|
let Some(modal) = self.power_modal.take() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
if cancel
|
||||||
|
&& let PowerAction::Logout(serial) = modal.action
|
||||||
|
&& let Some(control) = &self.control
|
||||||
|
{
|
||||||
|
control.cancel_logout(serial);
|
||||||
|
}
|
||||||
|
Task::batch(
|
||||||
|
modal
|
||||||
|
.surfaces
|
||||||
|
.into_iter()
|
||||||
|
.map(|surface| destroy_layer_surface(surface.id)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn cancel_logout_modal(&mut self, serial: u32) -> Task<Message> {
|
||||||
|
if self
|
||||||
|
.power_modal
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|modal| modal.action == PowerAction::Logout(serial))
|
||||||
|
{
|
||||||
|
self.destroy_power_modal(false)
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn execute_power_modal(&mut self) -> Task<Message> {
|
||||||
|
let Some(modal) = &mut self.power_modal else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
if modal.motion.closing() || self.pending_power.is_some() {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let action = match modal.action {
|
||||||
|
PowerAction::Logout(serial) => {
|
||||||
|
if let Some(control) = &self.control {
|
||||||
|
control.confirm_logout(serial);
|
||||||
|
return self.destroy_power_modal(false);
|
||||||
|
}
|
||||||
|
modal.error = Some("Ferese shell control is unavailable".to_owned());
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
PowerAction::Poweroff => status::Action::Poweroff,
|
||||||
|
PowerAction::Reboot => status::Action::Reboot,
|
||||||
|
PowerAction::Suspend => status::Action::Suspend,
|
||||||
|
};
|
||||||
|
let id = modal.surfaces[0].id;
|
||||||
|
self.pending_power = Some((id, modal.action));
|
||||||
|
let destroy = self.destroy_power_modal(false);
|
||||||
|
let execute = Task::perform(
|
||||||
|
async move {
|
||||||
|
tokio::task::spawn_blocking(move || status::execute_power(action))
|
||||||
|
.await
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
.and_then(|result| result)
|
||||||
|
},
|
||||||
|
move |result| cosmic::Action::App(Message::PowerCompleted(id, result)),
|
||||||
|
);
|
||||||
|
Task::batch([destroy, execute])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn finish_power_action(
|
||||||
|
&mut self,
|
||||||
|
id: window::Id,
|
||||||
|
result: Result<(), String>,
|
||||||
|
) -> Task<Message> {
|
||||||
|
let Some((pending_id, action)) = self.pending_power else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
if id != pending_id {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.pending_power = None;
|
||||||
|
match result {
|
||||||
|
Ok(()) => Task::none(),
|
||||||
|
Err(error) => {
|
||||||
|
let task = self.open_power_modal(action, None);
|
||||||
|
if let Some(modal) = &mut self.power_modal {
|
||||||
|
modal.error = Some(error);
|
||||||
|
}
|
||||||
|
task
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_power_modal(&self, id: window::Id) -> Element<'_, cosmic::Action<Message>> {
|
||||||
|
let Some(modal) = &self.power_modal else {
|
||||||
|
return text("").into();
|
||||||
|
};
|
||||||
|
let Some(surface) = modal.surfaces.iter().find(|surface| surface.id == id) else {
|
||||||
|
return text("").into();
|
||||||
|
};
|
||||||
|
let progress = modal.motion.progress();
|
||||||
|
let content: Element<'_, cosmic::Action<Message>> = if surface.primary {
|
||||||
|
let theme = self.config.theme;
|
||||||
|
let material = surface.effects.is_some();
|
||||||
|
let alpha = if material { 1.0 } else { progress };
|
||||||
|
let mut palette = theme.palette();
|
||||||
|
palette.text = palette.text.scale_alpha(alpha);
|
||||||
|
palette.muted = palette.muted.scale_alpha(alpha);
|
||||||
|
palette.sidebar = palette.sidebar.scale_alpha(alpha);
|
||||||
|
palette.card = palette.card.scale_alpha(alpha);
|
||||||
|
palette.accent = palette.accent.scale_alpha(alpha);
|
||||||
|
let mut rows = column![
|
||||||
|
text(modal.action.title()).font(shell_font()).size(23),
|
||||||
|
text(modal.action.description())
|
||||||
|
.font(shell_font())
|
||||||
|
.size(14)
|
||||||
|
.class(theme::Text::Color(palette.muted)),
|
||||||
|
]
|
||||||
|
.spacing(12);
|
||||||
|
if let Some(error) = &modal.error {
|
||||||
|
rows = rows.push(text(error).size(13));
|
||||||
|
}
|
||||||
|
let confirm = button::text(modal.action.label())
|
||||||
|
.class(ferese_theme::controls::button_style(palette, true))
|
||||||
|
.on_press(cosmic::Action::App(Message::ExecutePower));
|
||||||
|
rows = rows.push(
|
||||||
|
row![
|
||||||
|
Space::new().width(Length::Fill),
|
||||||
|
button::text("Cancel")
|
||||||
|
.class(ferese_theme::controls::button_style(palette, false))
|
||||||
|
.on_press(cosmic::Action::App(Message::CancelPower)),
|
||||||
|
confirm,
|
||||||
|
]
|
||||||
|
.spacing(10),
|
||||||
|
);
|
||||||
|
container(rows)
|
||||||
|
.id("ferese-blur-card")
|
||||||
|
.width(Length::Fill)
|
||||||
|
.max_width(440)
|
||||||
|
.padding(24)
|
||||||
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
|
background: (!material).then_some(Background::Color(color_with_opacity(
|
||||||
|
theme.surface_popover,
|
||||||
|
alpha,
|
||||||
|
))),
|
||||||
|
text_color: Some(palette.text),
|
||||||
|
icon_color: Some(palette.text),
|
||||||
|
border: Border {
|
||||||
|
color: palette.muted.scale_alpha(0.16),
|
||||||
|
width: 1.0,
|
||||||
|
radius: theme.material_radius.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
text("").into()
|
||||||
|
};
|
||||||
|
let content = if surface.primary {
|
||||||
|
motion::animated(
|
||||||
|
content,
|
||||||
|
progress,
|
||||||
|
surface.regions.clone(),
|
||||||
|
self.config.theme.material_radius,
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
};
|
||||||
|
let centered = container(content)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.padding(24)
|
||||||
|
.align_x(cosmic::iced::Alignment::Center)
|
||||||
|
.align_y(cosmic::iced::Alignment::Center)
|
||||||
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
|
background: Some(Background::Color(Color::from_rgba(
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.34 * progress,
|
||||||
|
))),
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
centered.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_power_actions_require_confirmation() {
|
||||||
|
assert_eq!(
|
||||||
|
PowerAction::from_status(status::Action::Poweroff),
|
||||||
|
Some(PowerAction::Poweroff)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
PowerAction::from_status(status::Action::Reboot),
|
||||||
|
Some(PowerAction::Reboot)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
PowerAction::from_status(status::Action::Suspend),
|
||||||
|
Some(PowerAction::Suspend)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
PowerAction::from_status(status::Action::Brightness(50)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(PowerAction::Logout(1).label(), "Log out");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user