Add display mode chooser and align screen-sharing styles
Open the four display modes from XF86Display or the external-monitor bar icon. Reuse output reconciliation and confirmation for manual layout changes, and notify the shell when monitor state changes. Release backdrop locks before render-element queries to prevent blurred popups from freezing the compositor. Cover popup open, update and close with regression tests. Share translucent card styles between display modes and capture sources, show focus rings during keyboard navigation, and align picker buttons, label sizing and footer spacing with system modals.
This commit is contained in:
1 parent
5100d533fb
commit
96f5ee442e
30 files changed
+1354
-72
No files matched your search
@@ -551,6 +551,7 @@ pub(crate) fn set_output_profile(state: &mut Ferese, name: &str) -> Result<(), S
|
||||
let known_good = backend.applied_outputs.clone();
|
||||
backend.manual_outputs.profile = (name != "auto").then(|| name.to_owned());
|
||||
backend.manual_outputs.internal = None;
|
||||
backend.manual_outputs.layout = None;
|
||||
let desired = crate::output_policy::select_profile(
|
||||
&backend.monitors,
|
||||
backend.lid.closed(),
|
||||
@@ -584,6 +585,47 @@ pub(crate) fn set_output_profile(state: &mut Ferese, name: &str) -> Result<(), S
|
||||
return Err("profile does not match the connected monitors/lid".into());
|
||||
}
|
||||
arm_output_confirmation(state, known_good, previous);
|
||||
state.notify_monitor_state();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_output_layout(state: &mut Ferese, layout: crate::config::OutputLayout) -> Result<(), String> {
|
||||
let backend = state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
.ok_or("display modes require the direct DRM backend")?;
|
||||
if !backend.active || backend.lid.pending() {
|
||||
return Err("output session is inactive or refreshing lid state".into());
|
||||
}
|
||||
let usable = backend
|
||||
.monitors
|
||||
.iter()
|
||||
.filter(|monitor| monitor.usable)
|
||||
.collect::<Vec<_>>();
|
||||
let available = match layout {
|
||||
crate::config::OutputLayout::InternalOnly => usable.iter().any(|monitor| monitor.internal),
|
||||
crate::config::OutputLayout::ExternalOnly => usable.iter().any(|monitor| !monitor.internal),
|
||||
crate::config::OutputLayout::Extend | crate::config::OutputLayout::Mirror => usable.len() >= 2,
|
||||
};
|
||||
if !available {
|
||||
return Err("this display mode requires monitors that are not connected".into());
|
||||
}
|
||||
let previous = backend.manual_outputs.clone();
|
||||
let known_good = backend.applied_outputs.clone();
|
||||
backend.manual_outputs.profile = None;
|
||||
backend.manual_outputs.internal = None;
|
||||
backend.manual_outputs.layout = Some(layout);
|
||||
if let Err(error) = validate_live_outputs(state, &state.output_profiles) {
|
||||
state.direct_backend.as_mut().unwrap().manual_outputs = previous;
|
||||
return Err(error);
|
||||
}
|
||||
reconcile_outputs(state, false);
|
||||
if let Some(error) = state.direct_backend.as_ref().unwrap().output_error.clone() {
|
||||
state.direct_backend.as_mut().unwrap().manual_outputs = previous;
|
||||
return Err(error);
|
||||
}
|
||||
arm_output_confirmation(state, known_good, previous);
|
||||
state.notify_monitor_state();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -606,12 +648,14 @@ pub(crate) fn set_internal_output(state: &mut Ferese, enabled: bool) -> Result<(
|
||||
let previous = backend.manual_outputs.clone();
|
||||
let known_good = backend.applied_outputs.clone();
|
||||
backend.manual_outputs.internal = Some(enabled);
|
||||
backend.manual_outputs.layout = None;
|
||||
reconcile_outputs(state, false);
|
||||
if let Some(error) = state.direct_backend.as_ref().unwrap().output_error.clone() {
|
||||
state.direct_backend.as_mut().unwrap().manual_outputs = previous;
|
||||
return Err(error);
|
||||
}
|
||||
arm_output_confirmation(state, known_good, previous);
|
||||
state.notify_monitor_state();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -672,6 +716,7 @@ pub(crate) fn confirm_output_configuration(state: &mut Ferese, confirm: bool) ->
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
state.notify_monitor_state();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2193,11 +2238,21 @@ fn refresh_monitor_policy(state: &mut Ferese) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let previous_override = backend.manual_outputs.profile.clone();
|
||||
let previous_override = (
|
||||
backend.manual_outputs.profile.clone(),
|
||||
backend.manual_outputs.internal,
|
||||
backend.manual_outputs.layout,
|
||||
);
|
||||
backend
|
||||
.manual_outputs
|
||||
.observe(&backend.monitors, backend.lid.closed(), &state.output_profiles);
|
||||
if previous_override != backend.manual_outputs.profile {
|
||||
if previous_override
|
||||
!= (
|
||||
backend.manual_outputs.profile.clone(),
|
||||
backend.manual_outputs.internal,
|
||||
backend.manual_outputs.layout,
|
||||
)
|
||||
{
|
||||
backend.confirmation = None;
|
||||
if let Some(token) = backend.confirmation_timer.take() {
|
||||
state.loop_handle.remove(token);
|
||||
|
||||
@@ -351,6 +351,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||
sync_battery_timer(state);
|
||||
state.restore_output_focus();
|
||||
state.relayout();
|
||||
state.notify_monitor_state();
|
||||
render_all(state);
|
||||
lid::apply_suspend_policy(state);
|
||||
}
|
||||
|
||||
@@ -386,6 +386,31 @@ pub(crate) fn config_path() -> Option<PathBuf> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn display_key_has_a_default_binding_and_can_be_rebound() {
|
||||
let bindings = parse("").runtime_config().unwrap().bindings;
|
||||
assert!(bindings.iter().any(
|
||||
|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_XF86Display)
|
||||
&& binding.action == BindingAction::ToggleDisplayMode
|
||||
));
|
||||
let bindings = parse(
|
||||
"binding keys=\"XF86Display\" disabled=#true\nbinding keys=\"Super+P\" action=\"toggle-display-mode\"\n",
|
||||
)
|
||||
.runtime_config()
|
||||
.unwrap()
|
||||
.bindings;
|
||||
assert!(
|
||||
!bindings
|
||||
.iter()
|
||||
.any(|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_XF86Display))
|
||||
);
|
||||
assert!(
|
||||
bindings
|
||||
.iter()
|
||||
.any(|binding| binding.modifiers.logo && binding.action == BindingAction::ToggleDisplayMode)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shortcut_hint_has_a_default_binding_and_can_be_rebound() {
|
||||
let bindings = parse("").runtime_config().unwrap().bindings;
|
||||
@@ -925,7 +950,7 @@ mod tests {
|
||||
let input = config.input_settings().unwrap();
|
||||
let bindings = config.bindings(&input).unwrap();
|
||||
|
||||
assert_eq!(bindings.len(), 53);
|
||||
assert_eq!(bindings.len(), 54);
|
||||
for (shift, action) in [
|
||||
(false, BindingAction::ToggleMaximized),
|
||||
(true, BindingAction::ToggleFullscreen),
|
||||
@@ -969,7 +994,7 @@ mod tests {
|
||||
|
||||
let input = replaced.input_settings().unwrap();
|
||||
let bindings = replaced.bindings(&input).unwrap();
|
||||
assert_eq!(bindings.len(), 53);
|
||||
assert_eq!(bindings.len(), 54);
|
||||
assert!(bindings.iter().any(|binding| {
|
||||
binding.action
|
||||
== BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()].into())
|
||||
@@ -977,7 +1002,7 @@ mod tests {
|
||||
|
||||
let input = unbound.input_settings().unwrap();
|
||||
let bindings = unbound.bindings(&input).unwrap();
|
||||
assert_eq!(bindings.len(), 52);
|
||||
assert_eq!(bindings.len(), 53);
|
||||
assert!(!bindings.iter().any(|binding| binding.action == BindingAction::Close));
|
||||
}
|
||||
|
||||
|
||||
@@ -336,6 +336,10 @@ pub(super) fn parse_action(
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleOverview)
|
||||
}
|
||||
"toggle-display-mode" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleDisplayMode)
|
||||
}
|
||||
"toggle-keybinding-guide" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleKeybindingGuide)
|
||||
@@ -380,6 +384,7 @@ pub(super) fn default_bindings() -> Vec<BindingConfig> {
|
||||
binding("Super+]", "expel", None),
|
||||
binding("Super+Shift+Space", "toggle-floating", None),
|
||||
binding("Super+Tab", "toggle-overview", None),
|
||||
binding("XF86Display", "toggle-display-mode", None),
|
||||
binding("Super+F1", "toggle-keybinding-guide", None),
|
||||
binding("Super+Escape", "workspace-back-and-forth", None),
|
||||
binding("Super+BackSpace", "focus-last-window", None),
|
||||
@@ -467,6 +472,7 @@ pub enum BindingAction {
|
||||
ToggleFloating,
|
||||
ToggleOverview,
|
||||
ToggleKeybindingGuide,
|
||||
ToggleDisplayMode,
|
||||
}
|
||||
|
||||
impl Binding {
|
||||
@@ -576,6 +582,7 @@ impl Binding {
|
||||
BindingAction::Expel => "Move window out of column".into(),
|
||||
BindingAction::ToggleFloating => "Toggle floating window".into(),
|
||||
BindingAction::ToggleOverview => "Open or close overview".into(),
|
||||
BindingAction::ToggleDisplayMode => "Open display mode chooser".into(),
|
||||
BindingAction::ToggleKeybindingGuide => "Open or close shortcut hint".into(),
|
||||
};
|
||||
let mut parts = Vec::new();
|
||||
|
||||
@@ -1072,6 +1072,9 @@ impl Ferese {
|
||||
BindingAction::Expel => self.expel_focused_window(),
|
||||
BindingAction::ToggleFloating => self.toggle_focused_floating(),
|
||||
BindingAction::ToggleOverview => self.toggle_overview(),
|
||||
BindingAction::ToggleDisplayMode => {
|
||||
self.toggle_display_mode();
|
||||
}
|
||||
BindingAction::ToggleKeybindingGuide => {
|
||||
self.toggle_keybinding_guide();
|
||||
}
|
||||
|
||||
@@ -660,6 +660,9 @@ impl Ferese {
|
||||
"toggle-maximized" => self.toggle_focused_maximized(),
|
||||
"toggle-layout" => self.toggle_layout_mode(),
|
||||
"toggle-overview" => self.toggle_overview(),
|
||||
"toggle-display-mode" => {
|
||||
self.toggle_display_mode();
|
||||
}
|
||||
"toggle-keybinding-guide" => {
|
||||
if !self.toggle_keybinding_guide() {
|
||||
return Err(CommandError::new(
|
||||
@@ -699,6 +702,7 @@ impl Ferese {
|
||||
return Ok(json!({
|
||||
"confirmation_pending": self.direct_backend.as_ref().is_some_and(|backend| backend.confirmation_pending()),
|
||||
"manual_profile": self.direct_backend.as_ref().and_then(|backend| backend.manual_outputs.profile.as_ref()),
|
||||
"manual_layout": self.direct_backend.as_ref().and_then(|backend| backend.manual_outputs.layout),
|
||||
"manual_internal": self.direct_backend.as_ref().and_then(|backend| backend.manual_outputs.internal),
|
||||
"profiles": self.output_profiles.iter().map(|profile| json!({
|
||||
"name": profile.name, "layout": profile.layout,
|
||||
@@ -720,6 +724,13 @@ impl Ferese {
|
||||
.map_err(|error| CommandError::new("output_configuration_failed", error))?;
|
||||
return Ok(self.outputs_json());
|
||||
}
|
||||
"output-layout" => {
|
||||
let layout = serde_json::from_value::<crate::config::OutputLayout>(args["layout"].clone())
|
||||
.map_err(|_| CommandError::new("invalid_arguments", "unknown display layout"))?;
|
||||
crate::backends::direct::set_output_layout(self, layout)
|
||||
.map_err(|error| CommandError::new("output_configuration_failed", error))?;
|
||||
return Ok(self.outputs_json());
|
||||
}
|
||||
"output-internal" => {
|
||||
let enabled = args["enabled"]
|
||||
.as_bool()
|
||||
|
||||
@@ -40,6 +40,7 @@ pub(crate) struct DesiredOutputConfiguration {
|
||||
pub(crate) struct ManualOverride {
|
||||
pub profile: Option<String>,
|
||||
pub internal: Option<bool>,
|
||||
pub layout: Option<OutputLayout>,
|
||||
topology: Option<(BTreeSet<String>, bool)>,
|
||||
}
|
||||
|
||||
@@ -55,6 +56,7 @@ impl ManualOverride {
|
||||
if self.topology.as_ref().is_some_and(|old| old != &topology) {
|
||||
self.profile = None;
|
||||
self.internal = None;
|
||||
self.layout = None;
|
||||
}
|
||||
if self.profile.as_ref().is_some_and(|name| {
|
||||
!profiles
|
||||
@@ -134,7 +136,9 @@ pub(crate) fn select_profile(
|
||||
})
|
||||
.map(|(_, profile)| profile)
|
||||
});
|
||||
let layout = selected.map_or(OutputLayout::Extend, |profile| profile.layout);
|
||||
let layout = manual
|
||||
.layout
|
||||
.unwrap_or_else(|| selected.map_or(OutputLayout::Extend, |profile| profile.layout));
|
||||
let manage_lid = selected.is_none_or(|profile| profile.lid_policy == LidPolicy::DockOrSuspend);
|
||||
let external = monitors.iter().any(|monitor| monitor.usable && !monitor.internal);
|
||||
let mut outputs = monitors
|
||||
@@ -149,6 +153,9 @@ pub(crate) fn select_profile(
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or_else(|| defaults(monitor.identity.clone()));
|
||||
if manual.layout.is_some() {
|
||||
settings.enabled = true;
|
||||
}
|
||||
settings.enabled &= monitor.usable;
|
||||
settings.enabled &= match layout {
|
||||
OutputLayout::InternalOnly => monitor.internal,
|
||||
@@ -465,6 +472,57 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_layouts_reuse_profiles_and_expire_on_monitor_changes() {
|
||||
let connected = [monitor("eDP-1", true), monitor("DP-1", false)];
|
||||
let mut settings = defaults("DP-1".into());
|
||||
settings.enabled = false;
|
||||
settings.scale = 1.5;
|
||||
let mut mobile = profile("mobile", &["eDP-1"], OutputLayout::InternalOnly);
|
||||
mobile.outputs.push(settings);
|
||||
let profiles = [mobile];
|
||||
for layout in [
|
||||
OutputLayout::InternalOnly,
|
||||
OutputLayout::ExternalOnly,
|
||||
OutputLayout::Extend,
|
||||
OutputLayout::Mirror,
|
||||
] {
|
||||
let mut manual = ManualOverride {
|
||||
layout: Some(layout),
|
||||
..ManualOverride::default()
|
||||
};
|
||||
manual.observe(&connected, false, &profiles);
|
||||
let desired = select_profile(&connected, false, &profiles, &manual);
|
||||
assert_eq!(
|
||||
desired.outputs[0].settings.enabled,
|
||||
layout != OutputLayout::ExternalOnly
|
||||
);
|
||||
assert_eq!(
|
||||
desired.outputs[1].settings.enabled,
|
||||
layout != OutputLayout::InternalOnly
|
||||
);
|
||||
assert_eq!(
|
||||
desired.outputs[1].settings.scale, 1.5,
|
||||
"manual layout must preserve configured scale"
|
||||
);
|
||||
assert_eq!(
|
||||
desired
|
||||
.outputs
|
||||
.iter()
|
||||
.filter(|output| output.mirror_source.is_some())
|
||||
.count(),
|
||||
usize::from(layout == OutputLayout::Mirror)
|
||||
);
|
||||
manual.observe(&connected[..1], false, &profiles);
|
||||
assert_eq!(manual.layout, None);
|
||||
assert!(
|
||||
select_profile(&connected[..1], false, &profiles, &manual).outputs[0]
|
||||
.settings
|
||||
.enabled
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn specificity_and_config_order() {
|
||||
let monitors = [monitor("eDP-1", true), monitor("DP-1", false)];
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(super) struct Backdrop {
|
||||
pub commit: CommitCounter,
|
||||
sources: Vec<Source>,
|
||||
region: Option<Rectangle<i32, Physical>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
struct Source {
|
||||
id: Id,
|
||||
commit: CommitCounter,
|
||||
@@ -19,9 +19,9 @@ struct Source {
|
||||
}
|
||||
|
||||
impl Backdrop {
|
||||
fn update<E: Element>(
|
||||
fn update<'a, E: Element + 'a>(
|
||||
&mut self,
|
||||
elements: &[E],
|
||||
elements: impl IntoIterator<Item = &'a E>,
|
||||
region: Rectangle<i32, Physical>,
|
||||
scale: RenderScale<f64>,
|
||||
) -> bool {
|
||||
@@ -90,15 +90,33 @@ impl Backdrop {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_shared<E: Element>(
|
||||
shared: &std::sync::Mutex<Backdrop>,
|
||||
elements: &[E],
|
||||
owner: &Id,
|
||||
region: Rectangle<i32, Physical>,
|
||||
scale: RenderScale<f64>,
|
||||
) -> bool {
|
||||
// Element queries can read this same backdrop's commit. Snapshot the state,
|
||||
// release its lock, then inspect sources. Rendering is the sole writer.
|
||||
let mut backdrop = shared.lock().unwrap().clone();
|
||||
let sources = elements.iter().filter(|element| element.id() != owner);
|
||||
let changed = backdrop.update(sources, region, scale);
|
||||
*shared.lock().unwrap() = backdrop;
|
||||
changed
|
||||
}
|
||||
|
||||
pub(super) fn update(elements: &[AnimatedWindowRenderElement], scale: f64) {
|
||||
// Resolve lower blurs first so their damage propagates to higher materials.
|
||||
for (index, element) in elements.iter().enumerate().rev() {
|
||||
if let AnimatedWindowRenderElement::Blur(blur) = element {
|
||||
let changed =
|
||||
blur.backdrop
|
||||
.lock()
|
||||
.unwrap()
|
||||
.update(&elements[index + 1..], blur.geometry(scale.into()), scale.into());
|
||||
let changed = update_shared(
|
||||
&blur.backdrop,
|
||||
&elements[index + 1..],
|
||||
blur.id(),
|
||||
blur.geometry(scale.into()),
|
||||
scale.into(),
|
||||
);
|
||||
if changed {
|
||||
blur.capture_dirty.store(true, Ordering::Relaxed);
|
||||
}
|
||||
@@ -115,6 +133,7 @@ mod tests {
|
||||
commit: CommitCounter,
|
||||
rect: Rectangle<i32, Physical>,
|
||||
damage: Vec<Rectangle<i32, Physical>>,
|
||||
probe: Option<Arc<std::sync::Mutex<Backdrop>>>,
|
||||
opaque: bool,
|
||||
alpha: f32,
|
||||
}
|
||||
@@ -126,6 +145,7 @@ mod tests {
|
||||
commit: CommitCounter::default(),
|
||||
rect,
|
||||
damage: vec![],
|
||||
probe: None,
|
||||
opaque: false,
|
||||
alpha: 1.0,
|
||||
}
|
||||
@@ -142,6 +162,9 @@ mod tests {
|
||||
&self.id
|
||||
}
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
if let Some(backdrop) = &self.probe {
|
||||
let _guard = backdrop.try_lock().expect("backdrop lock held during an element query");
|
||||
}
|
||||
self.commit
|
||||
}
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
@@ -173,6 +196,41 @@ mod tests {
|
||||
Rectangle::new((x, y).into(), (w, h).into())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blurred_popup_open_update_close_releases_backdrop_before_element_queries() {
|
||||
let region = rect(0, 0, 100, 100);
|
||||
let backdrop = Arc::new(std::sync::Mutex::new(Backdrop::default()));
|
||||
let owner = Id::new();
|
||||
let mut panel = TestElement::new(region);
|
||||
panel.probe = Some(backdrop.clone());
|
||||
let mut popup_alias = TestElement::new(region);
|
||||
popup_alias.id = owner.clone();
|
||||
let mut elements = [popup_alias, panel];
|
||||
|
||||
// Open: ignore a repeated instance of the cached popup material, and
|
||||
// allow dependencies to query backdrop state without a nested lock.
|
||||
assert!(update_shared(&backdrop, &elements, &owner, region, 1.0.into()));
|
||||
assert_eq!(backdrop.lock().unwrap().sources.len(), 1);
|
||||
assert!(!update_shared(&backdrop, &elements, &owner, region, 1.0.into()));
|
||||
|
||||
// Update both content and bounds while the popup remains visible.
|
||||
elements[1].repaint(vec![rect(10, 10, 5, 5)]);
|
||||
assert!(update_shared(&backdrop, &elements, &owner, region, 1.0.into()));
|
||||
assert!(update_shared(
|
||||
&backdrop,
|
||||
&elements,
|
||||
&owner,
|
||||
rect(5, 5, 90, 90),
|
||||
1.0.into()
|
||||
));
|
||||
|
||||
// Close and reopen: dependencies disappear, then are tracked again.
|
||||
assert!(update_shared::<TestElement>(&backdrop, &[], &owner, region, 1.0.into()));
|
||||
assert!(backdrop.lock().unwrap().sources.is_empty());
|
||||
assert!(update_shared(&backdrop, &elements, &owner, region, 1.0.into()));
|
||||
assert!(!update_shared(&backdrop, &elements, &owner, region, 1.0.into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_damage_intersecting_the_sample_invalidates_its_output() {
|
||||
let mut first = Backdrop::default();
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::private_client::ClientCapabilities;
|
||||
use crate::state::ClientState;
|
||||
|
||||
pub(crate) fn init_global(display: &DisplayHandle) {
|
||||
display.create_global::<Ferese, FereseShellManagerV1, _>(5, ());
|
||||
display.create_global::<Ferese, FereseShellManagerV1, _>(6, ());
|
||||
}
|
||||
|
||||
pub(crate) fn config_chunks(source: &str) -> Vec<&str> {
|
||||
@@ -206,6 +206,34 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn notify_monitor_state(&self) {
|
||||
for shell in self
|
||||
.shell_resources
|
||||
.iter()
|
||||
.filter_map(|shell| shell.upgrade().ok())
|
||||
.filter(|shell| shell.version() >= 6)
|
||||
{
|
||||
shell.monitor_state_changed();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_display_mode(&mut self) -> bool {
|
||||
if self.session_lock.active() {
|
||||
return false;
|
||||
}
|
||||
let Some(shell) = self
|
||||
.shell_resources
|
||||
.iter()
|
||||
.filter_map(|shell| shell.upgrade().ok())
|
||||
.find(|shell| shell.version() >= 6)
|
||||
else {
|
||||
tracing::warn!("display mode chooser requires the updated Ferese shell");
|
||||
return false;
|
||||
};
|
||||
shell.toggle_display_mode(self.focused_output().map_or_else(String::new, |output| output.name()));
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_keybinding_guide(&mut self) -> bool {
|
||||
if self.session_lock.active() {
|
||||
return false;
|
||||
|
||||
Reference in new issue
Block a user