Fix screenshots and workspace switching; add configurable swipe bindings
This commit is contained in:
1 parent
5d6c610fec
commit
dbdfc5688e
14 files changed
+707
-101
No files matched your search
@@ -20,11 +20,12 @@ and native Settings app.
|
|||||||
|
|
||||||
- Scrolling columns, tree tiling and floating windows
|
- Scrolling columns, tree tiling and floating windows
|
||||||
- Workspace overview
|
- Workspace overview
|
||||||
|
- Configurable three-finger workspace and window navigation
|
||||||
- Configurable animations
|
- Configurable animations
|
||||||
- Built-in bar and control center
|
- Built-in bar and control center
|
||||||
- Native Settings app
|
- Native Settings app
|
||||||
- Desktop widgets
|
- Desktop widgets
|
||||||
- Area screenshots saved to disk and copied to the clipboard
|
- Area and full-screen screenshots with Satty annotation
|
||||||
- Themes, blur and gradient borders
|
- Themes, blur and gradient borders
|
||||||
- Multi-monitor support and fractional scaling
|
- Multi-monitor support and fractional scaling
|
||||||
- Live configuration reload
|
- Live configuration reload
|
||||||
@@ -51,7 +52,7 @@ Requirements (Fedora package names):
|
|||||||
- Default terminal: `foot` (or configure another terminal).
|
- Default terminal: `foot` (or configure another terminal).
|
||||||
- Optional: `zenity` for wallpaper browsing, `swayidle` for idle actions,
|
- Optional: `zenity` for wallpaper browsing, `swayidle` for idle actions,
|
||||||
and `swaylock` or `swaylock-effects` for locking.
|
and `swaylock` or `swaylock-effects` for locking.
|
||||||
- Screenshots: `grim`, `slurp`, and `wl-clipboard`.
|
- Screenshots: `grim`, `slurp`, `satty`, and `wl-clipboard`.
|
||||||
|
|
||||||
For a login-screen session alongside your existing desktop:
|
For a login-screen session alongside your existing desktop:
|
||||||
|
|
||||||
@@ -69,6 +70,13 @@ session logs and rollback.
|
|||||||
|
|
||||||
Built-in defaults (your configuration can override them):
|
Built-in defaults (your configuration can override them):
|
||||||
|
|
||||||
|
The bar shows workspaces 1–9 in a compact numbered strip, including empty ones.
|
||||||
|
Click a number to switch using the same behavior as Super+1–9. A filled button marks this monitor's
|
||||||
|
active workspace; an outlined button marks one active on another monitor.
|
||||||
|
Three-finger swipes up/down navigate workspaces on this monitor; left/right
|
||||||
|
navigate windows. Assign swipes to any keyboard-binding action (including
|
||||||
|
overview, move, or a launcher command) in **Settings → Shortcuts**.
|
||||||
|
|
||||||
| Shortcut | Action |
|
| Shortcut | Action |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Super + Enter | Open a terminal |
|
| Super + Enter | Open a terminal |
|
||||||
@@ -80,10 +88,13 @@ Built-in defaults (your configuration can override them):
|
|||||||
| Super + Tab | Open workspace and window overview |
|
| Super + Tab | Open workspace and window overview |
|
||||||
| Super + M | Switch scrolling / tree layout |
|
| Super + M | Switch scrolling / tree layout |
|
||||||
| Super + Shift + S | Select a screenshot area |
|
| Super + Shift + S | Select a screenshot area |
|
||||||
|
| Print Screen / Fn + PrtSc | Capture the whole screen |
|
||||||
| Super + Shift + E | Log out immediately—save your work first |
|
| Super + Shift + E | Log out immediately—save your work first |
|
||||||
|
|
||||||
In an installed session, screenshots are saved to `Pictures/Screenshots` and
|
Both screenshot shortcuts open Satty for annotation. Press Enter in Satty to
|
||||||
copied to the clipboard. Press Escape to cancel the selection.
|
save to `Pictures/Screenshots` and copy the edited image to the clipboard;
|
||||||
|
Escape discards it. Escape also cancels an area selection.
|
||||||
|
Run `ferese-screenshot --full` to capture the whole screen from a terminal.
|
||||||
When focus follows the mouse is enabled, hovering changes keyboard focus without
|
When focus follows the mouse is enabled, hovering changes keyboard focus without
|
||||||
raising the window; clicking raises it within its layer.
|
raising the window; clicking raises it within its layer.
|
||||||
|
|
||||||
|
|||||||
+168
-3
@@ -440,6 +440,10 @@ pub struct Binding {
|
|||||||
enum BindingTrigger {
|
enum BindingTrigger {
|
||||||
Keysym(u32),
|
Keysym(u32),
|
||||||
Physical(Keycode),
|
Physical(Keycode),
|
||||||
|
Swipe {
|
||||||
|
fingers: u32,
|
||||||
|
direction: crate::gestures::SwipeDirection,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||||
@@ -452,6 +456,7 @@ struct BindingModifiers {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum BindingAction {
|
pub enum BindingAction {
|
||||||
|
None,
|
||||||
Spawn(Vec<String>),
|
Spawn(Vec<String>),
|
||||||
Close,
|
Close,
|
||||||
Exit,
|
Exit,
|
||||||
@@ -459,6 +464,7 @@ pub enum BindingAction {
|
|||||||
Move(ferese_layout::Direction),
|
Move(ferese_layout::Direction),
|
||||||
Resize(ferese_layout::Direction),
|
Resize(ferese_layout::Direction),
|
||||||
SwitchWorkspace(u8),
|
SwitchWorkspace(u8),
|
||||||
|
SwitchRelativeWorkspace(bool),
|
||||||
MoveToWorkspace(u8),
|
MoveToWorkspace(u8),
|
||||||
ToggleFullscreen,
|
ToggleFullscreen,
|
||||||
ToggleMaximized,
|
ToggleMaximized,
|
||||||
@@ -491,8 +497,21 @@ impl Binding {
|
|||||||
&& match self.trigger {
|
&& match self.trigger {
|
||||||
BindingTrigger::Keysym(expected) => keysyms.contains(&expected),
|
BindingTrigger::Keysym(expected) => keysyms.contains(&expected),
|
||||||
BindingTrigger::Physical(expected) => expected == keycode,
|
BindingTrigger::Physical(expected) => expected == keycode,
|
||||||
|
BindingTrigger::Swipe { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn swipe_fingers(&self, count: u32) -> bool {
|
||||||
|
matches!(self.trigger, BindingTrigger::Swipe { fingers, .. } if fingers == count)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn matches_swipe(
|
||||||
|
&self,
|
||||||
|
count: u32,
|
||||||
|
target: crate::gestures::SwipeDirection,
|
||||||
|
) -> bool {
|
||||||
|
matches!(self.trigger, BindingTrigger::Swipe { fingers, direction } if fingers == count && direction == target)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq)]
|
||||||
@@ -501,6 +520,7 @@ enum BindingMatch {
|
|||||||
#[default]
|
#[default]
|
||||||
Keysym,
|
Keysym,
|
||||||
Physical,
|
Physical,
|
||||||
|
Swipe,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
@@ -530,6 +550,11 @@ pub struct TouchpadSettings {
|
|||||||
pub tap: bool,
|
pub tap: bool,
|
||||||
pub natural_scroll: bool,
|
pub natural_scroll: bool,
|
||||||
pub disable_while_typing: bool,
|
pub disable_while_typing: bool,
|
||||||
|
pub swipe_threshold: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_swipe_threshold() -> u16 {
|
||||||
|
80
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -572,6 +597,8 @@ struct TouchpadConfig {
|
|||||||
natural_scroll: bool,
|
natural_scroll: bool,
|
||||||
#[serde(default = "enabled_by_default")]
|
#[serde(default = "enabled_by_default")]
|
||||||
disable_while_typing: bool,
|
disable_while_typing: bool,
|
||||||
|
#[serde(default = "default_swipe_threshold")]
|
||||||
|
swipe_threshold: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TouchpadConfig {
|
impl Default for TouchpadConfig {
|
||||||
@@ -580,6 +607,7 @@ impl Default for TouchpadConfig {
|
|||||||
tap: true,
|
tap: true,
|
||||||
natural_scroll: true,
|
natural_scroll: true,
|
||||||
disable_while_typing: true,
|
disable_while_typing: true,
|
||||||
|
swipe_threshold: default_swipe_threshold(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -879,6 +907,12 @@ impl Config {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !(16..=1000).contains(&self.input.touchpad.swipe_threshold) {
|
||||||
|
return Err(ConfigError::InvalidInputValue {
|
||||||
|
field: "touchpad.swipe_threshold",
|
||||||
|
value: self.input.touchpad.swipe_threshold.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
Ok(InputSettings {
|
Ok(InputSettings {
|
||||||
focus_follows_mouse: self.input.focus_follows_mouse,
|
focus_follows_mouse: self.input.focus_follows_mouse,
|
||||||
xkb_layout: self.input.xkb_layout.clone(),
|
xkb_layout: self.input.xkb_layout.clone(),
|
||||||
@@ -890,6 +924,7 @@ impl Config {
|
|||||||
tap: self.input.touchpad.tap,
|
tap: self.input.touchpad.tap,
|
||||||
natural_scroll: self.input.touchpad.natural_scroll,
|
natural_scroll: self.input.touchpad.natural_scroll,
|
||||||
disable_while_typing: self.input.touchpad.disable_while_typing,
|
disable_while_typing: self.input.touchpad.disable_while_typing,
|
||||||
|
swipe_threshold: self.input.touchpad.swipe_threshold,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -901,6 +936,10 @@ impl Config {
|
|||||||
"screenshot".to_owned(),
|
"screenshot".to_owned(),
|
||||||
vec!["ferese-screenshot".to_owned()],
|
vec!["ferese-screenshot".to_owned()],
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"screenshot-full".to_owned(),
|
||||||
|
vec!["ferese-screenshot".to_owned(), "--full".to_owned()],
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
commands.extend(self.commands.clone());
|
commands.extend(self.commands.clone());
|
||||||
validate_commands(&commands)?;
|
validate_commands(&commands)?;
|
||||||
@@ -1184,6 +1223,15 @@ fn parse_binding(
|
|||||||
let trigger = match identity.match_mode {
|
let trigger = match identity.match_mode {
|
||||||
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
|
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
|
||||||
BindingMatch::Physical => BindingTrigger::Physical(Keycode::new(identity.key)),
|
BindingMatch::Physical => BindingTrigger::Physical(Keycode::new(identity.key)),
|
||||||
|
BindingMatch::Swipe => BindingTrigger::Swipe {
|
||||||
|
fingers: identity.key / 4,
|
||||||
|
direction: [
|
||||||
|
crate::gestures::SwipeDirection::Up,
|
||||||
|
crate::gestures::SwipeDirection::Down,
|
||||||
|
crate::gestures::SwipeDirection::Left,
|
||||||
|
crate::gestures::SwipeDirection::Right,
|
||||||
|
][(identity.key % 4) as usize],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Binding {
|
Ok(Binding {
|
||||||
@@ -1198,6 +1246,20 @@ fn binding_identity(
|
|||||||
keymap: &xkb::Keymap,
|
keymap: &xkb::Keymap,
|
||||||
) -> Result<BindingIdentity, ConfigError> {
|
) -> Result<BindingIdentity, ConfigError> {
|
||||||
let (modifiers, key) = parse_chord(&configured.keys)?;
|
let (modifiers, key) = parse_chord(&configured.keys)?;
|
||||||
|
if let Some((fingers, direction)) = parse_swipe(&key) {
|
||||||
|
if configured.match_mode == BindingMatch::Physical
|
||||||
|
|| modifiers != BindingModifiers::default()
|
||||||
|
{
|
||||||
|
return Err(ConfigError::InvalidBinding(
|
||||||
|
"swipes cannot use keyboard modifiers or physical key matching".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Ok(BindingIdentity {
|
||||||
|
modifiers,
|
||||||
|
match_mode: BindingMatch::Swipe,
|
||||||
|
key: fingers * 4 + direction as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
let key = match configured.match_mode {
|
let key = match configured.match_mode {
|
||||||
BindingMatch::Keysym => parse_keysym(&key)?,
|
BindingMatch::Keysym => parse_keysym(&key)?,
|
||||||
BindingMatch::Physical => keymap
|
BindingMatch::Physical => keymap
|
||||||
@@ -1209,6 +1271,11 @@ fn binding_identity(
|
|||||||
configured.keys
|
configured.keys
|
||||||
))
|
))
|
||||||
})?,
|
})?,
|
||||||
|
BindingMatch::Swipe => {
|
||||||
|
return Err(ConfigError::InvalidBinding(
|
||||||
|
"gesture keys must use Swipe3Left/Right/Up/Down (3–5 fingers)".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(BindingIdentity {
|
Ok(BindingIdentity {
|
||||||
@@ -1222,6 +1289,9 @@ fn binding_identity_for_runtime(binding: &Binding) -> BindingIdentity {
|
|||||||
let (match_mode, key) = match binding.trigger {
|
let (match_mode, key) = match binding.trigger {
|
||||||
BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key),
|
BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key),
|
||||||
BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()),
|
BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()),
|
||||||
|
BindingTrigger::Swipe { fingers, direction } => {
|
||||||
|
(BindingMatch::Swipe, fingers * 4 + direction as u32)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
BindingIdentity {
|
BindingIdentity {
|
||||||
@@ -1269,6 +1339,24 @@ fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), ConfigError> {
|
|||||||
Ok((modifiers, key))
|
Ok((modifiers, key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_swipe(key: &str) -> Option<(u32, crate::gestures::SwipeDirection)> {
|
||||||
|
use crate::gestures::SwipeDirection;
|
||||||
|
let lower = key.to_ascii_lowercase();
|
||||||
|
let rest = lower.strip_prefix("swipe")?;
|
||||||
|
let fingers = rest.get(..1)?.parse::<u32>().ok()?;
|
||||||
|
if !(3..=5).contains(&fingers) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let direction = match rest.get(1..)? {
|
||||||
|
"up" => SwipeDirection::Up,
|
||||||
|
"down" => SwipeDirection::Down,
|
||||||
|
"left" => SwipeDirection::Left,
|
||||||
|
"right" => SwipeDirection::Right,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some((fingers, direction))
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_keysym(name: &str) -> Result<u32, ConfigError> {
|
fn parse_keysym(name: &str) -> Result<u32, ConfigError> {
|
||||||
let normalized = match name {
|
let normalized = match name {
|
||||||
"Enter" | "enter" => "Return".to_owned(),
|
"Enter" | "enter" => "Return".to_owned(),
|
||||||
@@ -1312,6 +1400,16 @@ fn parse_action(
|
|||||||
};
|
};
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
|
"none" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::None)
|
||||||
|
}
|
||||||
|
"workspace-next" | "workspace-previous" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::SwitchRelativeWorkspace(
|
||||||
|
action == "workspace-next",
|
||||||
|
))
|
||||||
|
}
|
||||||
"spawn" => {
|
"spawn" => {
|
||||||
let command = required_argument()?;
|
let command = required_argument()?;
|
||||||
let argv = commands.get(command).ok_or_else(|| {
|
let argv = commands.get(command).ok_or_else(|| {
|
||||||
@@ -1405,6 +1503,11 @@ fn default_bindings() -> Vec<BindingConfig> {
|
|||||||
let mut bindings = vec![
|
let mut bindings = vec![
|
||||||
binding("Super+Enter", "spawn", Some("terminal")),
|
binding("Super+Enter", "spawn", Some("terminal")),
|
||||||
binding("Super+Shift+S", "spawn", Some("screenshot")),
|
binding("Super+Shift+S", "spawn", Some("screenshot")),
|
||||||
|
binding("Print", "spawn", Some("screenshot-full")),
|
||||||
|
binding("Swipe3Up", "workspace-next", None),
|
||||||
|
binding("Swipe3Down", "workspace-previous", None),
|
||||||
|
binding("Swipe3Left", "focus", Some("right")),
|
||||||
|
binding("Swipe3Right", "focus", Some("left")),
|
||||||
binding("Super+Q", "close", None),
|
binding("Super+Q", "close", None),
|
||||||
binding("Super+F", "toggle-maximized", None),
|
binding("Super+F", "toggle-maximized", None),
|
||||||
binding("Super+Shift+F", "toggle-fullscreen", None),
|
binding("Super+Shift+F", "toggle-fullscreen", None),
|
||||||
@@ -2054,6 +2157,7 @@ mod tests {
|
|||||||
tap: false,
|
tap: false,
|
||||||
natural_scroll: false,
|
natural_scroll: false,
|
||||||
disable_while_typing: true,
|
disable_while_typing: true,
|
||||||
|
swipe_threshold: 80,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -2070,13 +2174,65 @@ mod tests {
|
|||||||
assert!(delay.input_settings().is_err());
|
assert!(delay.input_settings().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swipe_bindings_can_override_actions_disable_defaults_and_set_distance() {
|
||||||
|
use crate::gestures::SwipeDirection;
|
||||||
|
let config = parse(
|
||||||
|
"[input.touchpad]\nswipe_threshold = 120\n[[bindings]]\nkeys = 'Swipe3Up'\naction = 'toggle-overview'\n[[bindings]]\nkeys = 'Swipe3Left'\naction = 'move'\nargument = 'left'\n[[bindings]]\nkeys = 'Swipe3Down'\ndisabled = true",
|
||||||
|
);
|
||||||
|
let input = config.input_settings().unwrap();
|
||||||
|
assert_eq!(input.touchpad.swipe_threshold, 120);
|
||||||
|
let bindings = config.bindings(&input).unwrap();
|
||||||
|
assert!(
|
||||||
|
bindings
|
||||||
|
.iter()
|
||||||
|
.any(|binding| binding.matches_swipe(3, SwipeDirection::Up)
|
||||||
|
&& binding.action == BindingAction::ToggleOverview)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
bindings
|
||||||
|
.iter()
|
||||||
|
.any(|binding| binding.matches_swipe(3, SwipeDirection::Left)
|
||||||
|
&& binding.action == BindingAction::Move(ferese_layout::Direction::Left))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!bindings
|
||||||
|
.iter()
|
||||||
|
.any(|binding| binding.matches_swipe(3, SwipeDirection::Down))
|
||||||
|
);
|
||||||
|
for threshold in [0, 15, 1001] {
|
||||||
|
assert!(
|
||||||
|
parse(&format!("[input.touchpad]\nswipe_threshold = {threshold}"))
|
||||||
|
.input_settings()
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gesture_bindings_validate_fingers_directions_and_actions() {
|
||||||
|
for keys in ["Swipe2Up", "Swipe3Diagonal", "Super+Swipe3Up"] {
|
||||||
|
let config = parse(&format!(
|
||||||
|
"[[bindings]]\nkeys = '{keys}'\naction = 'toggle-overview'"
|
||||||
|
));
|
||||||
|
assert!(config.bindings(&config.input_settings().unwrap()).is_err());
|
||||||
|
}
|
||||||
|
let config = parse("[[bindings]]\nkeys = 'Swipe4Up'\naction = 'toggle-overview'");
|
||||||
|
let bindings = config.bindings(&config.input_settings().unwrap()).unwrap();
|
||||||
|
assert!(
|
||||||
|
bindings
|
||||||
|
.iter()
|
||||||
|
.any(|binding| binding.matches_swipe(4, crate::gestures::SwipeDirection::Up))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn supplies_complete_v0_bindings_and_terminal_command() {
|
fn supplies_complete_v0_bindings_and_terminal_command() {
|
||||||
let config = parse("");
|
let config = parse("");
|
||||||
let input = config.input_settings().unwrap();
|
let input = config.input_settings().unwrap();
|
||||||
let bindings = config.bindings(&input).unwrap();
|
let bindings = config.bindings(&input).unwrap();
|
||||||
|
|
||||||
assert_eq!(bindings.len(), 43);
|
assert_eq!(bindings.len(), 48);
|
||||||
for (shift, action) in [
|
for (shift, action) in [
|
||||||
(false, BindingAction::ToggleMaximized),
|
(false, BindingAction::ToggleMaximized),
|
||||||
(true, BindingAction::ToggleFullscreen),
|
(true, BindingAction::ToggleFullscreen),
|
||||||
@@ -2102,6 +2258,15 @@ mod tests {
|
|||||||
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_s)
|
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_s)
|
||||||
&& binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned()])
|
&& binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned()])
|
||||||
}));
|
}));
|
||||||
|
assert!(bindings.iter().any(|binding| {
|
||||||
|
binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Print)
|
||||||
|
&& binding.modifiers == BindingModifiers::default()
|
||||||
|
&& binding.action
|
||||||
|
== BindingAction::Spawn(vec![
|
||||||
|
"ferese-screenshot".to_owned(),
|
||||||
|
"--full".to_owned(),
|
||||||
|
])
|
||||||
|
}));
|
||||||
assert!(bindings.iter().any(|binding| {
|
assert!(bindings.iter().any(|binding| {
|
||||||
binding.modifiers.logo
|
binding.modifiers.logo
|
||||||
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Tab)
|
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Tab)
|
||||||
@@ -2118,7 +2283,7 @@ mod tests {
|
|||||||
|
|
||||||
let input = replaced.input_settings().unwrap();
|
let input = replaced.input_settings().unwrap();
|
||||||
let bindings = replaced.bindings(&input).unwrap();
|
let bindings = replaced.bindings(&input).unwrap();
|
||||||
assert_eq!(bindings.len(), 43);
|
assert_eq!(bindings.len(), 48);
|
||||||
assert!(bindings.iter().any(|binding| {
|
assert!(bindings.iter().any(|binding| {
|
||||||
binding.action
|
binding.action
|
||||||
== BindingAction::Spawn(vec![
|
== BindingAction::Spawn(vec![
|
||||||
@@ -2130,7 +2295,7 @@ mod tests {
|
|||||||
|
|
||||||
let input = unbound.input_settings().unwrap();
|
let input = unbound.input_settings().unwrap();
|
||||||
let bindings = unbound.bindings(&input).unwrap();
|
let bindings = unbound.bindings(&input).unwrap();
|
||||||
assert_eq!(bindings.len(), 42);
|
assert_eq!(bindings.len(), 47);
|
||||||
assert!(
|
assert!(
|
||||||
!bindings
|
!bindings
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
//! A swipe commits once on release; cancelled and ambiguous gestures do nothing.
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub(crate) enum SwipeDirection {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct Swipe {
|
||||||
|
delta: Option<(f64, f64)>,
|
||||||
|
threshold: f64,
|
||||||
|
fingers: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Swipe {
|
||||||
|
pub fn begin(&mut self, fingers: u32, blocked: bool, threshold: u16) {
|
||||||
|
self.delta = ((3..=5).contains(&fingers) && !blocked).then_some((0.0, 0.0));
|
||||||
|
self.threshold = f64::from(threshold);
|
||||||
|
self.fingers = fingers;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fingers(&self) -> u32 {
|
||||||
|
self.fingers
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active(&self) -> bool {
|
||||||
|
self.delta.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, dx: f64, dy: f64) {
|
||||||
|
if let Some((x, y)) = &mut self.delta {
|
||||||
|
*x += dx;
|
||||||
|
*y += dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(&mut self, cancelled: bool) -> Option<SwipeDirection> {
|
||||||
|
let (x, y) = self.delta.take()?;
|
||||||
|
if cancelled || !x.is_finite() || !y.is_finite() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
const DOMINANCE: f64 = 1.25;
|
||||||
|
if x.abs() >= self.threshold && x.abs() > y.abs() * DOMINANCE {
|
||||||
|
Some(if x < 0.0 {
|
||||||
|
SwipeDirection::Left
|
||||||
|
} else {
|
||||||
|
SwipeDirection::Right
|
||||||
|
})
|
||||||
|
} else if y.abs() >= self.threshold && y.abs() > x.abs() * DOMINANCE {
|
||||||
|
Some(if y < 0.0 {
|
||||||
|
SwipeDirection::Up
|
||||||
|
} else {
|
||||||
|
SwipeDirection::Down
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_swipe_accumulates_motion_and_commits_only_once_on_release() {
|
||||||
|
let mut swipe = Swipe::default();
|
||||||
|
swipe.begin(3, false, 80);
|
||||||
|
for _ in 0..10 {
|
||||||
|
swipe.update(-10.0, 1.0);
|
||||||
|
}
|
||||||
|
assert!(swipe.active());
|
||||||
|
assert_eq!(swipe.finish(false), Some(SwipeDirection::Left));
|
||||||
|
assert!(!swipe.active());
|
||||||
|
assert_eq!(swipe.finish(false), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vertical_and_horizontal_swipes_are_distinct() {
|
||||||
|
for (delta, expected) in [
|
||||||
|
((0.0, -100.0), SwipeDirection::Up),
|
||||||
|
((0.0, 100.0), SwipeDirection::Down),
|
||||||
|
((-100.0, 0.0), SwipeDirection::Left),
|
||||||
|
((100.0, 0.0), SwipeDirection::Right),
|
||||||
|
] {
|
||||||
|
let mut swipe = Swipe::default();
|
||||||
|
swipe.begin(3, false, 80);
|
||||||
|
swipe.update(delta.0, delta.1);
|
||||||
|
assert_eq!(swipe.finish(false), Some(expected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cancellation_jitter_diagonals_and_nonfinite_motion_do_not_navigate() {
|
||||||
|
for (x, y, cancelled) in [
|
||||||
|
(200.0, 0.0, true),
|
||||||
|
(79.0, 0.0, false),
|
||||||
|
(100.0, 100.0, false),
|
||||||
|
(f64::NAN, 100.0, false),
|
||||||
|
] {
|
||||||
|
let mut swipe = Swipe::default();
|
||||||
|
swipe.begin(3, false, 80);
|
||||||
|
swipe.update(x, y);
|
||||||
|
assert_eq!(swipe.finish(cancelled), None);
|
||||||
|
assert!(!swipe.active());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn other_finger_counts_and_blocked_gestures_are_not_intercepted() {
|
||||||
|
for (fingers, blocked) in [(2, false), (6, false), (3, true)] {
|
||||||
|
let mut swipe = Swipe::default();
|
||||||
|
swipe.begin(fingers, blocked, 80);
|
||||||
|
assert!(!swipe.active());
|
||||||
|
swipe.update(200.0, 0.0);
|
||||||
|
assert_eq!(swipe.finish(false), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn configured_distance_changes_when_a_swipe_commits() {
|
||||||
|
let mut swipe = Swipe::default();
|
||||||
|
swipe.begin(3, false, 120);
|
||||||
|
swipe.update(100.0, 0.0);
|
||||||
|
assert_eq!(swipe.finish(false), None);
|
||||||
|
swipe.begin(3, false, 40);
|
||||||
|
swipe.update(100.0, 0.0);
|
||||||
|
assert_eq!(swipe.finish(false), Some(SwipeDirection::Right));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::{
|
backend::{allocator::Fourcc, renderer::ExportMem},
|
||||||
allocator::Fourcc,
|
|
||||||
renderer::{ExportMem, TextureMapping},
|
|
||||||
},
|
|
||||||
output::Output,
|
output::Output,
|
||||||
reexports::{
|
reexports::{
|
||||||
wayland_protocols_wlr::screencopy::v1::server::{
|
wayland_protocols_wlr::screencopy::v1::server::{
|
||||||
@@ -355,8 +352,8 @@ where
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve the renderer's row order and report it through the protocol
|
// Smithay's output projection already accounts for OpenGL's Y axis.
|
||||||
// instead of making another full-frame copy on the compositor thread.
|
// These rows are in output-buffer order, including its output transform.
|
||||||
unsafe {
|
unsafe {
|
||||||
std::ptr::copy_nonoverlapping(
|
std::ptr::copy_nonoverlapping(
|
||||||
source.as_ptr(),
|
source.as_ptr(),
|
||||||
@@ -377,12 +374,11 @@ where
|
|||||||
.frame
|
.frame
|
||||||
.damage(0, 0, expected.w as u32, expected.h as u32);
|
.damage(0, 0, expected.w as u32, expected.h as u32);
|
||||||
}
|
}
|
||||||
let flags = if mapping.flipped() {
|
// TextureMapping::flipped describes importing the mapping as a texture;
|
||||||
zwlr_screencopy_frame_v1::Flags::YInvert
|
// using it here would make clients flip the already-correct output rows.
|
||||||
} else {
|
capture
|
||||||
zwlr_screencopy_frame_v1::Flags::empty()
|
.frame
|
||||||
};
|
.flags(zwlr_screencopy_frame_v1::Flags::empty());
|
||||||
capture.frame.flags(flags);
|
|
||||||
|
|
||||||
let seconds = timestamp.as_secs();
|
let seconds = timestamp.as_secs();
|
||||||
capture.frame.ready(
|
capture.frame.ready(
|
||||||
|
|||||||
+129
-4
@@ -3,13 +3,17 @@ use std::process::Command;
|
|||||||
|
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::input::{
|
backend::input::{
|
||||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
|
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent,
|
||||||
KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
|
GestureEndEvent, GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState,
|
||||||
Switch, SwitchState, SwitchToggleEvent, TouchEvent,
|
KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, Switch,
|
||||||
|
SwitchState, SwitchToggleEvent, TouchEvent,
|
||||||
},
|
},
|
||||||
input::{
|
input::{
|
||||||
keyboard::{FilterResult, keysyms},
|
keyboard::{FilterResult, keysyms},
|
||||||
pointer::{AxisFrame, ButtonEvent, MotionEvent, PointerHandle, RelativeMotionEvent},
|
pointer::{
|
||||||
|
AxisFrame, ButtonEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
|
||||||
|
GestureSwipeUpdateEvent, MotionEvent, PointerHandle, RelativeMotionEvent,
|
||||||
|
},
|
||||||
touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent},
|
touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent},
|
||||||
},
|
},
|
||||||
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||||
@@ -43,6 +47,71 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
|
InputEvent::GestureSwipeBegin { event } => {
|
||||||
|
let pointer = seat.get_pointer().expect("seat has a pointer");
|
||||||
|
self.swipe.begin(
|
||||||
|
event.fingers(),
|
||||||
|
self.swipe_navigation_blocked()
|
||||||
|
|| !self
|
||||||
|
.bindings
|
||||||
|
.iter()
|
||||||
|
.any(|binding| binding.swipe_fingers(event.fingers())),
|
||||||
|
self.input_settings.touchpad.swipe_threshold,
|
||||||
|
);
|
||||||
|
if self.swipe.active() {
|
||||||
|
self.focus_output_at(pointer.current_location());
|
||||||
|
} else {
|
||||||
|
pointer.gesture_swipe_begin(
|
||||||
|
self,
|
||||||
|
&GestureSwipeBeginEvent {
|
||||||
|
serial: SERIAL_COUNTER.next_serial(),
|
||||||
|
time: event.time() as u32,
|
||||||
|
fingers: event.fingers(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputEvent::GestureSwipeUpdate { event } => {
|
||||||
|
if self.swipe.active() {
|
||||||
|
self.swipe.update(event.delta_x(), event.delta_y());
|
||||||
|
} else {
|
||||||
|
seat.get_pointer()
|
||||||
|
.expect("seat has a pointer")
|
||||||
|
.gesture_swipe_update(
|
||||||
|
self,
|
||||||
|
&GestureSwipeUpdateEvent {
|
||||||
|
time: event.time() as u32,
|
||||||
|
delta: event.delta(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputEvent::GestureSwipeEnd { event } => {
|
||||||
|
if self.swipe.active() {
|
||||||
|
let fingers = self.swipe.fingers();
|
||||||
|
let cancelled = event.cancelled() || self.swipe_navigation_blocked();
|
||||||
|
if let Some(direction) = self.swipe.finish(cancelled)
|
||||||
|
&& let Some(action) = self
|
||||||
|
.bindings
|
||||||
|
.iter()
|
||||||
|
.find(|binding| binding.matches_swipe(fingers, direction))
|
||||||
|
.map(|binding| binding.action.clone())
|
||||||
|
{
|
||||||
|
self.execute_binding(action);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
seat.get_pointer()
|
||||||
|
.expect("seat has a pointer")
|
||||||
|
.gesture_swipe_end(
|
||||||
|
self,
|
||||||
|
&GestureSwipeEndEvent {
|
||||||
|
serial: SERIAL_COUNTER.next_serial(),
|
||||||
|
time: event.time() as u32,
|
||||||
|
cancelled: event.cancelled(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
InputEvent::SwitchToggle { event } if event.switch() == Some(Switch::Lid) => {
|
InputEvent::SwitchToggle { event } if event.switch() == Some(Switch::Lid) => {
|
||||||
crate::backends::direct::set_lid_closed(self, event.state() == SwitchState::On);
|
crate::backends::direct::set_lid_closed(self, event.state() == SwitchState::On);
|
||||||
}
|
}
|
||||||
@@ -387,6 +456,60 @@ impl Ferese {
|
|||||||
Some(event.position_transformed(geometry.size) + geometry.loc.to_f64())
|
Some(event.position_transformed(geometry.size) + geometry.loc.to_f64())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn swipe_navigation_blocked(&self) -> bool {
|
||||||
|
self.session_lock.active
|
||||||
|
|| self.active_shortcuts_inhibitor.is_some()
|
||||||
|
|| self
|
||||||
|
.seat
|
||||||
|
.get_pointer()
|
||||||
|
.is_some_and(|pointer| pointer.is_grabbed())
|
||||||
|
|| self
|
||||||
|
.seat
|
||||||
|
.get_keyboard()
|
||||||
|
.is_some_and(|keyboard| keyboard.is_grabbed())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn switch_relative_workspace(&mut self, next: bool) {
|
||||||
|
let Some(output) = self.output_workspaces.focused_output() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(current) = self.output_workspaces.active_workspace(output) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut candidates = self
|
||||||
|
.workspaces
|
||||||
|
.iter()
|
||||||
|
.filter(|workspace| {
|
||||||
|
self.output_workspaces
|
||||||
|
.output_for_workspace(workspace.id)
|
||||||
|
.is_none_or(|owner| owner == output)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
candidates.sort_by_key(|workspace| {
|
||||||
|
(
|
||||||
|
workspace.name.parse::<u32>().unwrap_or(u32::MAX),
|
||||||
|
workspace.id.0,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let Some(index) = candidates
|
||||||
|
.iter()
|
||||||
|
.position(|workspace| workspace.id == current)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let index = if next {
|
||||||
|
index.checked_add(1)
|
||||||
|
} else {
|
||||||
|
index.checked_sub(1)
|
||||||
|
};
|
||||||
|
if let Some(workspace) = index
|
||||||
|
.and_then(|index| candidates.get(index))
|
||||||
|
.map(|workspace| workspace.id)
|
||||||
|
{
|
||||||
|
self.activate_managed_workspace(workspace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn focus_window_at(&mut self, position: Point<f64, Logical>, serial: Serial, raise: bool) {
|
fn focus_window_at(&mut self, position: Point<f64, Logical>, serial: Serial, raise: bool) {
|
||||||
if self.session_lock.active {
|
if self.session_lock.active {
|
||||||
self.focus_output_at(position);
|
self.focus_output_at(position);
|
||||||
@@ -562,6 +685,7 @@ impl Ferese {
|
|||||||
|
|
||||||
fn execute_binding(&mut self, action: BindingAction) {
|
fn execute_binding(&mut self, action: BindingAction) {
|
||||||
match action {
|
match action {
|
||||||
|
BindingAction::None => {}
|
||||||
BindingAction::Spawn(mut argv) => {
|
BindingAction::Spawn(mut argv) => {
|
||||||
let program = argv.remove(0);
|
let program = argv.remove(0);
|
||||||
|
|
||||||
@@ -584,6 +708,7 @@ impl Ferese {
|
|||||||
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),
|
||||||
|
BindingAction::SwitchRelativeWorkspace(next) => self.switch_relative_workspace(next),
|
||||||
BindingAction::SwitchWorkspace(workspace) => {
|
BindingAction::SwitchWorkspace(workspace) => {
|
||||||
self.switch_workspace(u32::from(workspace));
|
self.switch_workspace(u32::from(workspace));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ mod cursor;
|
|||||||
mod daemon;
|
mod daemon;
|
||||||
mod dimming;
|
mod dimming;
|
||||||
mod effects;
|
mod effects;
|
||||||
|
mod gestures;
|
||||||
mod grabs;
|
mod grabs;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
mod input;
|
mod input;
|
||||||
|
|||||||
+15
-14
@@ -167,6 +167,7 @@ pub struct Ferese {
|
|||||||
pub(crate) cursor_theme: xcursor::CursorTheme,
|
pub(crate) cursor_theme: xcursor::CursorTheme,
|
||||||
pub(crate) named_cursors: HashMap<CursorIcon, crate::cursor::NamedCursor>,
|
pub(crate) named_cursors: HashMap<CursorIcon, crate::cursor::NamedCursor>,
|
||||||
pub intercepted_keys: HashSet<smithay::input::keyboard::Keycode>,
|
pub intercepted_keys: HashSet<smithay::input::keyboard::Keycode>,
|
||||||
|
pub(crate) swipe: crate::gestures::Swipe,
|
||||||
pub idle_inhibitors: HashMap<WlSurface, usize>,
|
pub idle_inhibitors: HashMap<WlSurface, usize>,
|
||||||
pub active_shortcuts_inhibitor: Option<KeyboardShortcutsInhibitor>,
|
pub active_shortcuts_inhibitor: Option<KeyboardShortcutsInhibitor>,
|
||||||
pub direct_backend: Option<crate::backends::direct::DirectBackendState>,
|
pub direct_backend: Option<crate::backends::direct::DirectBackendState>,
|
||||||
@@ -640,6 +641,7 @@ impl Ferese {
|
|||||||
cursor_theme,
|
cursor_theme,
|
||||||
named_cursors,
|
named_cursors,
|
||||||
intercepted_keys: HashSet::new(),
|
intercepted_keys: HashSet::new(),
|
||||||
|
swipe: crate::gestures::Swipe::default(),
|
||||||
idle_inhibitors: HashMap::new(),
|
idle_inhibitors: HashMap::new(),
|
||||||
active_shortcuts_inhibitor: None,
|
active_shortcuts_inhibitor: None,
|
||||||
direct_backend: None,
|
direct_backend: None,
|
||||||
@@ -682,6 +684,11 @@ impl Ferese {
|
|||||||
xdg_shell_state,
|
xdg_shell_state,
|
||||||
xdg_toplevel_icon_manager,
|
xdg_toplevel_icon_manager,
|
||||||
};
|
};
|
||||||
|
// Keep the numbered shortcuts and bar selectors available from startup,
|
||||||
|
// including empty workspaces that have not been assigned to an output.
|
||||||
|
for index in 1..=9 {
|
||||||
|
state.workspaces.ensure_numeric(index)?;
|
||||||
|
}
|
||||||
state._ipc_socket = Some(crate::ipc::init(event_loop)?);
|
state._ipc_socket = Some(crate::ipc::init(event_loop)?);
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
@@ -2364,25 +2371,19 @@ impl Ferese {
|
|||||||
|
|
||||||
let output = self
|
let output = self
|
||||||
.output_workspaces
|
.output_workspaces
|
||||||
.output_for_workspace(workspace)
|
.focused_output()
|
||||||
.or_else(|| self.output_workspaces.focused_output());
|
.or_else(|| self.output_workspaces.output_for_workspace(workspace));
|
||||||
let Some(output) = output else {
|
let Some(output) = output else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if self
|
let owner = match self.output_workspaces.switch_workspace(output, workspace) {
|
||||||
.output_workspaces
|
Ok(ferese_core::WorkspaceSwitch::Activated(output))
|
||||||
.output_for_workspace(workspace)
|
| Ok(ferese_core::WorkspaceSwitch::FocusedExisting(output)) => output,
|
||||||
.is_none()
|
Err(_) => return false,
|
||||||
&& self
|
};
|
||||||
.output_workspaces
|
|
||||||
.assign_workspace(output, workspace)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.activate_output_workspace(output, workspace);
|
self.activate_output_workspace(owner, workspace);
|
||||||
self.relayout();
|
self.relayout();
|
||||||
self.restore_keyboard_focus();
|
self.restore_keyboard_focus();
|
||||||
true
|
true
|
||||||
|
|||||||
+51
-6
@@ -134,10 +134,47 @@ angle = 135.0
|
|||||||
| `repeat_rate` | integer > 0 | `25` | Repeats per second |
|
| `repeat_rate` | integer > 0 | `25` | Repeats per second |
|
||||||
| `repeat_delay_ms` | integer ≥ 0 | `600` | Delay before repeat |
|
| `repeat_delay_ms` | integer ≥ 0 | `600` | Delay before repeat |
|
||||||
|
|
||||||
`[input.touchpad]` has three boolean keys, all defaulting to `true`: `tap`,
|
The touchpad device keys are booleans, all defaulting to `true`: `tap`,
|
||||||
`natural_scroll` and `disable_while_typing`. These apply to DRM devices; nested
|
`natural_scroll` and `disable_while_typing`. These apply to DRM devices; nested
|
||||||
previews use the host's physical input settings.
|
previews use the host's physical input settings.
|
||||||
|
|
||||||
|
Three-finger swipes navigate on release: up goes to the next workspace on the
|
||||||
|
current monitor, down to the previous; left focuses the window to the right,
|
||||||
|
and right focuses the window to the left. Workspace swipes skip workspaces owned
|
||||||
|
by other monitors and stop at the first/last workspace. Cancelled, short and
|
||||||
|
diagonal swipes do nothing. Navigation pauses while locked, during
|
||||||
|
window grabs, or when an application inhibits shortcuts.
|
||||||
|
|
||||||
|
Assign swipes in **Settings → Shortcuts** using the same actions and arguments as
|
||||||
|
keyboard bindings. Choose a "Customize swipe" button to add an override. The
|
||||||
|
gesture keys are `Swipe3Up`, `Swipe3Down`, `Swipe3Left`, and `Swipe3Right`; 4 or 5
|
||||||
|
fingers are also supported. Changes reload live:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[bindings]]
|
||||||
|
keys = "Swipe3Up"
|
||||||
|
action = "toggle-overview"
|
||||||
|
|
||||||
|
[[bindings]]
|
||||||
|
keys = "Swipe3Left"
|
||||||
|
action = "move"
|
||||||
|
argument = "left"
|
||||||
|
|
||||||
|
[[bindings]]
|
||||||
|
keys = "Swipe3Down"
|
||||||
|
disabled = true
|
||||||
|
```
|
||||||
|
|
||||||
|
`spawn` uses a named entry from `[commands]`, just like keyboard shortcuts. To
|
||||||
|
disable a direction in Settings, set its action to `none` and leave Argument
|
||||||
|
empty. In TOML, `disabled = true` removes the default binding. Swipes do not use
|
||||||
|
keyboard modifiers or physical key matching. The trigger is automatically
|
||||||
|
recognized from its name. Gesture navigation uses native touchpad events in a
|
||||||
|
hardware session.
|
||||||
|
|
||||||
|
Adjust recognition distance in **Settings → Keyboard & mouse**, or with
|
||||||
|
`swipe_threshold = 80` under `[input.touchpad]` (integer 16–1000 logical pixels).
|
||||||
|
|
||||||
## Commands and bindings
|
## Commands and bindings
|
||||||
|
|
||||||
`[commands]` maps arbitrary names to nonempty argument arrays. The built-in
|
`[commands]` maps arbitrary names to nonempty argument arrays. The built-in
|
||||||
@@ -145,7 +182,7 @@ previews use the host's physical input settings.
|
|||||||
|
|
||||||
| `[[bindings]]` key | Type / values | Default |
|
| `[[bindings]]` key | Type / values | Default |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `keys` | chord string, e.g. `"Super+Enter"` | required |
|
| `keys` | chord or gesture, e.g. `"Super+Enter"`, `"Swipe3Up"` | required |
|
||||||
| `match` | `"keysym"`, `"physical"` | `"keysym"` |
|
| `match` | `"keysym"`, `"physical"` | `"keysym"` |
|
||||||
| `action` | action name below | required unless disabled |
|
| `action` | action name below | required unless disabled |
|
||||||
| `argument` | string | required only for actions listed below |
|
| `argument` | string | required only for actions listed below |
|
||||||
@@ -161,6 +198,8 @@ invalid. A disabled binding must omit `action` and `argument`.
|
|||||||
| `spawn` | Name in `[commands]` |
|
| `spawn` | Name in `[commands]` |
|
||||||
| `focus`, `move`, `resize` | `"left"`, `"right"`, `"up"`, `"down"` |
|
| `focus`, `move`, `resize` | `"left"`, `"right"`, `"up"`, `"down"` |
|
||||||
| `workspace`, `move-to-workspace` | Workspace number string, 1–255 |
|
| `workspace`, `move-to-workspace` | Workspace number string, 1–255 |
|
||||||
|
| `workspace-next`, `workspace-previous` | None; next/previous workspace on this monitor |
|
||||||
|
| `none` | None; ignore this trigger |
|
||||||
| `close`, `exit`, `toggle-maximized`, `toggle-fullscreen`, `toggle-layout`, `cycle-column-width`, `center-column`, `consume`, `expel`, `toggle-floating`, `toggle-overview` | None |
|
| `close`, `exit`, `toggle-maximized`, `toggle-fullscreen`, `toggle-layout`, `cycle-column-width`, `center-column`, `consume`, `expel`, `toggle-floating`, `toggle-overview` | None |
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
@@ -178,13 +217,19 @@ Super+Shift+H/J/K/L move; Super+Ctrl+H/J/K/L resize; Super+1–9 workspace;
|
|||||||
Super+Shift+1–9 move to workspace; Super+R width cycle; Super+C center;
|
Super+Shift+1–9 move to workspace; Super+R width cycle; Super+C center;
|
||||||
Super+[/] consume/expel; Super+F maximize; Super+Shift+F fullscreen;
|
Super+[/] consume/expel; Super+F maximize; Super+Shift+F fullscreen;
|
||||||
Super+M layout; Super+Shift+Space floating; Super+Tab overview;
|
Super+M layout; Super+Shift+Space floating; Super+Tab overview;
|
||||||
Super+Shift+S area screenshot; Super+Shift+E immediate logout.
|
Super+Shift+S area screenshot; Print Screen whole-screen screenshot;
|
||||||
|
Super+Shift+E immediate logout.
|
||||||
|
|
||||||
The screenshot shortcut runs `ferese-screenshot`: drag to select an area, or
|
The screenshot shortcut runs `ferese-screenshot`: drag to select an area, or
|
||||||
press Escape to cancel. Captures are saved under your Pictures directory in
|
press Escape to cancel. Captures open in Satty for annotation. Press Enter to
|
||||||
`Screenshots` and copied to the clipboard as PNG images. It requires `grim`,
|
save the edited PNG under your Pictures directory in `Screenshots` and copy it
|
||||||
`slurp`, and `wl-copy` (from `wl-clipboard`). Override the `screenshot` command
|
to the clipboard; Escape discards the capture. It requires `grim`, `slurp`,
|
||||||
|
`satty`, and `wl-copy` (from `wl-clipboard`). Override the `screenshot` command
|
||||||
to use another screenshot tool.
|
to use another screenshot tool.
|
||||||
|
Print Screen runs `ferese-screenshot --full` and captures all enabled outputs
|
||||||
|
without a selector. Fn+PrtSc works when the keyboard emits the Print Screen key;
|
||||||
|
Fn is handled by the keyboard firmware. Override `screenshot-full` to customize
|
||||||
|
this command. Both modes open Satty with the same save and copy workflow.
|
||||||
|
|
||||||
## Window rules
|
## Window rules
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
On Fedora with SDDM, install a Rust toolchain (1.93 or newer), native build
|
On Fedora with SDDM, install a Rust toolchain (1.93 or newer), native build
|
||||||
dependencies, `desktop-file-utils`, D-Bus and `foot`. Wallpaper browsing also
|
dependencies, `desktop-file-utils`, D-Bus and `foot`. Wallpaper browsing also
|
||||||
needs `zenity`; locking needs swaylock or swaylock-effects.
|
needs `zenity`; locking needs swaylock or swaylock-effects.
|
||||||
Area screenshots need `grim`, `slurp`, and `wl-clipboard`.
|
Screenshots need `grim`, `slurp`, `satty`, and `wl-clipboard`.
|
||||||
|
|
||||||
From the checkout, run as your normal user:
|
From the checkout, run as your normal user:
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,15 @@
|
|||||||
[commands]
|
[commands]
|
||||||
terminal = ["foot"]
|
terminal = ["foot"]
|
||||||
screenshot = ["ferese-screenshot"]
|
screenshot = ["ferese-screenshot"]
|
||||||
|
screenshot-full = ["ferese-screenshot", "--full"]
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
xkb_layout = "us"
|
xkb_layout = "us"
|
||||||
focus_follows_mouse = false
|
focus_follows_mouse = false
|
||||||
|
|
||||||
|
[input.touchpad]
|
||||||
|
swipe_threshold = 80
|
||||||
|
|
||||||
[layout]
|
[layout]
|
||||||
smart_gaps = false
|
smart_gaps = false
|
||||||
|
|
||||||
@@ -38,3 +42,10 @@ margin_y = 80
|
|||||||
time_size = 72.0
|
time_size = 72.0
|
||||||
date_size = 18.0
|
date_size = 18.0
|
||||||
opacity = 0.9
|
opacity = 0.9
|
||||||
|
|
||||||
|
# Swipe bindings use the same actions and arguments as keyboard bindings.
|
||||||
|
# Defaults: up/down switch workspaces; left/right focus windows.
|
||||||
|
# Example override:
|
||||||
|
# [[bindings]]
|
||||||
|
# keys = "Swipe3Up"
|
||||||
|
# action = "toggle-overview"
|
||||||
+37
-13
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Select an area, save it in Pictures/Screenshots, and copy it to the clipboard.
|
# Capture an area or the whole desktop and open it in Satty for annotation.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
umask 077
|
umask 077
|
||||||
|
|
||||||
@@ -11,26 +11,50 @@ fail() {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
for tool in slurp grim wl-copy flock; do
|
mode=area
|
||||||
|
case ${1:-} in
|
||||||
|
'') ;;
|
||||||
|
--full) mode=full; shift ;;
|
||||||
|
--help|-h)
|
||||||
|
echo 'Usage: ferese-screenshot [--full]'
|
||||||
|
echo 'Select an area, or use --full for the whole desktop, then edit in Satty.'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) fail 'Usage: ferese-screenshot [--full]' ;;
|
||||||
|
esac
|
||||||
|
(($# == 0)) || fail 'Usage: ferese-screenshot [--full]'
|
||||||
|
|
||||||
|
for tool in grim satty wl-copy flock; do
|
||||||
command -v "$tool" >/dev/null || fail "Required command not found: $tool"
|
command -v "$tool" >/dev/null || fail "Required command not found: $tool"
|
||||||
done
|
done
|
||||||
: "${XDG_RUNTIME_DIR:?A Wayland login session is required}"
|
: "${XDG_RUNTIME_DIR:?A Wayland login session is required}"
|
||||||
exec 9>"$XDG_RUNTIME_DIR/ferese-screenshot.lock"
|
exec 9>"$XDG_RUNTIME_DIR/ferese-screenshot.lock"
|
||||||
flock -n 9 || exit 0
|
flock -n 9 || exit 0
|
||||||
|
|
||||||
# Escape cancels without creating a file or changing the clipboard.
|
capture_args=()
|
||||||
geometry=$(slurp) || exit 0
|
if [[ $mode == area ]]; then
|
||||||
[[ -n $geometry ]] || exit 0
|
command -v slurp >/dev/null || fail 'Required command not found: slurp'
|
||||||
|
# Escape cancels without creating a file or changing the clipboard.
|
||||||
|
geometry=$(slurp) || exit 0
|
||||||
|
[[ -n $geometry ]] || exit 0
|
||||||
|
capture_args=(-g "$geometry")
|
||||||
|
fi
|
||||||
pictures=$(xdg-user-dir PICTURES 2>/dev/null) || pictures="$HOME/Pictures"
|
pictures=$(xdg-user-dir PICTURES 2>/dev/null) || pictures="$HOME/Pictures"
|
||||||
directory=${pictures:-$HOME/Pictures}/Screenshots
|
directory=${pictures:-$HOME/Pictures}/Screenshots
|
||||||
mkdir -p -- "$directory" || fail 'Could not create the screenshot directory.'
|
mkdir -p -- "$directory" || fail 'Could not create the screenshot directory.'
|
||||||
file=$(mktemp "$directory/Screenshot-$(date +%Y-%m-%d_%H-%M-%S)-XXXXXX.png") || fail 'Could not create the screenshot file.'
|
file=$(mktemp "$directory/Screenshot-$(date +%Y-%m-%d_%H-%M-%S)-XXXXXX.png") || fail 'Could not create the screenshot file.'
|
||||||
trap 'rm -f -- "$file"' EXIT
|
input=''
|
||||||
grim -g "$geometry" "$file" || fail 'Could not capture the screen. Log back into Ferese after enabling screen capture.'
|
trap '[[ -z $input ]] || rm -f -- "$input"; [[ -s $file ]] || rm -f -- "$file"' EXIT
|
||||||
trap - EXIT
|
input=$(mktemp "$XDG_RUNTIME_DIR/ferese-screenshot-XXXXXX.png") || fail 'Could not create the temporary capture.'
|
||||||
# Do not pass the selector lock to wl-copy's background clipboard process.
|
grim "${capture_args[@]}" "$input" || fail 'Could not capture the screen. Log back into Ferese after enabling screen capture.'
|
||||||
wl-copy --type image/png <"$file" 9>&- || fail "Saved to $file, but could not copy to the clipboard."
|
# Enter saves the annotated image and copies it; Escape discards the capture.
|
||||||
if command -v notify-send >/dev/null; then
|
# Do not pass the selector lock to Satty or its clipboard subprocess.
|
||||||
notify-send --app-name=Ferese 'Screenshot saved and copied' "$file" || true
|
satty --filename "$input" --output-filename "$file" \
|
||||||
|
--fullscreen current-screen \
|
||||||
|
--copy-command 'wl-copy --type image/png' \
|
||||||
|
--save-after-copy \
|
||||||
|
--actions-on-enter save-to-file,save-to-clipboard,exit \
|
||||||
|
--actions-on-escape exit 9>&- || fail 'Could not open the screenshot in Satty.'
|
||||||
|
if [[ -s $file ]]; then
|
||||||
|
printf '%s\n' "$file"
|
||||||
fi
|
fi
|
||||||
printf '%s\n' "$file"
|
|
||||||
@@ -67,6 +67,7 @@ enum Message {
|
|||||||
WallpaperPicked(Result<Option<String>, String>),
|
WallpaperPicked(Result<Option<String>, String>),
|
||||||
NewCommand(String),
|
NewCommand(String),
|
||||||
AddCommand,
|
AddCommand,
|
||||||
|
AddSwipe(&'static str),
|
||||||
Remove(String, usize),
|
Remove(String, usize),
|
||||||
Thumbnail(String, Result<widget::image::Handle, String>),
|
Thumbnail(String, Result<widget::image::Handle, String>),
|
||||||
}
|
}
|
||||||
@@ -432,6 +433,22 @@ impl cosmic::Application for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::AddSwipe(keys) if !self.saving => {
|
||||||
|
let (action, argument) = match keys {
|
||||||
|
"Swipe3Up" => ("workspace-next", None),
|
||||||
|
"Swipe3Down" => ("workspace-previous", None),
|
||||||
|
"Swipe3Left" => ("focus", Some("right")),
|
||||||
|
_ => ("focus", Some("left")),
|
||||||
|
};
|
||||||
|
let mut fields = vec![
|
||||||
|
("keys".into(), keys.into()),
|
||||||
|
("action".into(), action.into()),
|
||||||
|
];
|
||||||
|
if let Some(argument) = argument {
|
||||||
|
fields.push(("argument".into(), argument.into()));
|
||||||
|
}
|
||||||
|
return self.change(Edit::Add("bindings".into(), fields));
|
||||||
|
}
|
||||||
Message::Remove(table, index) if !self.saving => {
|
Message::Remove(table, index) if !self.saving => {
|
||||||
self.inputs.clear();
|
self.inputs.clear();
|
||||||
let task = self.change(Edit::Remove(table, index));
|
let task = self.change(Edit::Remove(table, index));
|
||||||
@@ -705,7 +722,27 @@ impl cosmic::Application for App {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
Page::Shortcuts => {
|
Page::Shortcuts => {
|
||||||
body = body.push(self.note("Custom shortcuts from your config. Built-in shortcuts remain available unless overridden. Edit a field, then press Enter or move focus to save."));
|
body = body.push(self.note("Keyboard and swipe bindings share the same actions. Use Swipe3Up, Swipe3Down, Swipe3Left, or Swipe3Right as a shortcut (also supports 4 or 5 fingers). Actions include toggle-overview, spawn, focus, move, workspace-next, workspace-previous, and none. Leave Argument empty for actions such as toggle-overview or none."));
|
||||||
|
let mut gestures = row([]).spacing(6);
|
||||||
|
for (keys, label) in [
|
||||||
|
("Swipe3Up", "Customize swipe up"),
|
||||||
|
("Swipe3Down", "Customize swipe down"),
|
||||||
|
("Swipe3Left", "Customize swipe left"),
|
||||||
|
("Swipe3Right", "Customize swipe right"),
|
||||||
|
] {
|
||||||
|
let exists = (0..self.draft.records("bindings")).any(|index| {
|
||||||
|
self.draft
|
||||||
|
.string(&format!("bindings.{index}.keys"), "")
|
||||||
|
.eq_ignore_ascii_case(keys)
|
||||||
|
});
|
||||||
|
if !exists {
|
||||||
|
gestures =
|
||||||
|
gestures.push(button::standard(label).on_press_maybe(
|
||||||
|
(!self.saving).then_some(Message::AddSwipe(keys)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body = body.push(gestures);
|
||||||
for index in 0..self.draft.records("bindings") {
|
for index in 0..self.draft.records("bindings") {
|
||||||
let prefix = format!("bindings.{index}");
|
let prefix = format!("bindings.{index}");
|
||||||
let group = column([])
|
let group = column([])
|
||||||
@@ -713,13 +750,13 @@ impl cosmic::Application for App {
|
|||||||
.push(self.field(schema::text(
|
.push(self.field(schema::text(
|
||||||
format!("{prefix}.keys"),
|
format!("{prefix}.keys"),
|
||||||
"Shortcut",
|
"Shortcut",
|
||||||
"For example: Super+Return",
|
"For example: Super+Return or Swipe3Up",
|
||||||
"",
|
"",
|
||||||
)))
|
)))
|
||||||
.push(self.field(schema::text(
|
.push(self.field(schema::text(
|
||||||
format!("{prefix}.action"),
|
format!("{prefix}.action"),
|
||||||
"Action",
|
"Action",
|
||||||
"Ferese command, such as spawn or focus.",
|
"Action such as toggle-overview, spawn, focus, move, or none.",
|
||||||
"",
|
"",
|
||||||
)))
|
)))
|
||||||
.push(self.field(schema::text(
|
.push(self.field(schema::text(
|
||||||
|
|||||||
@@ -857,6 +857,17 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"Avoid accidental pointer movement.",
|
"Avoid accidental pointer movement.",
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
|
range(
|
||||||
|
"input.touchpad.swipe_threshold",
|
||||||
|
"Swipe distance",
|
||||||
|
"Distance needed to navigate. Larger values reduce accidental swipes.",
|
||||||
|
80.0,
|
||||||
|
16.0,
|
||||||
|
1000.0,
|
||||||
|
8.0,
|
||||||
|
" px",
|
||||||
|
true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
Page::Shortcuts | Page::Startup | Page::Displays => vec![],
|
Page::Shortcuts | Page::Startup | Page::Displays => vec![],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1649,7 +1649,6 @@ impl FereseShell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let focused_output = self.output_for_bar(id);
|
let focused_output = self.output_for_bar(id);
|
||||||
let focused_output_id = focused_output.map(|output| output.id);
|
|
||||||
let shell_theme = self.config.theme.for_bar();
|
let shell_theme = self.config.theme.for_bar();
|
||||||
let bar = BarMetrics::from(shell_theme);
|
let bar = BarMetrics::from(shell_theme);
|
||||||
let mut workspace_row = row::with_capacity(self.snapshot.workspaces.len() + 1)
|
let mut workspace_row = row::with_capacity(self.snapshot.workspaces.len() + 1)
|
||||||
@@ -1665,23 +1664,33 @@ impl FereseShell {
|
|||||||
self.overview_active,
|
self.overview_active,
|
||||||
1.0,
|
1.0,
|
||||||
));
|
));
|
||||||
for workspace in self
|
let mut workspace_buttons = row::with_capacity(self.snapshot.workspaces.len())
|
||||||
.snapshot
|
.spacing(1)
|
||||||
.workspaces
|
.align_y(cosmic::iced::Alignment::Center);
|
||||||
.iter()
|
for workspace in &self.snapshot.workspaces {
|
||||||
.filter(|workspace| workspace_on_bar(workspace.output, focused_output_id))
|
let active = workspace_active_on_bar(workspace.id, focused_output);
|
||||||
{
|
let owner = workspace
|
||||||
|
.output
|
||||||
|
.and_then(|id| self.snapshot.outputs.iter().find(|output| output.id == id));
|
||||||
let occupied = self
|
let occupied = self
|
||||||
.snapshot
|
.snapshot
|
||||||
.windows
|
.windows
|
||||||
.iter()
|
.iter()
|
||||||
.any(|window| window.workspace == workspace.id);
|
.any(|window| window.workspace == workspace.id);
|
||||||
let indicator = workspace_indicator(workspace.active, occupied, bar, shell_theme);
|
let indicator = workspace_indicator(
|
||||||
|
&workspace.name,
|
||||||
|
active,
|
||||||
|
workspace.active && !active,
|
||||||
|
occupied,
|
||||||
|
bar,
|
||||||
|
shell_theme,
|
||||||
|
);
|
||||||
let workspace_button = button::custom(indicator)
|
let workspace_button = button::custom(indicator)
|
||||||
.name(format!(
|
.name(format!(
|
||||||
"Workspace {}{}",
|
"Workspace {}{}{}",
|
||||||
workspace.name,
|
workspace.name,
|
||||||
if workspace.active { ", active" } else { "" }
|
if active { ", active" } else { "" },
|
||||||
|
owner.map_or(String::new(), |output| format!(", on {}", output.name))
|
||||||
))
|
))
|
||||||
.height(bar.control_height)
|
.height(bar.control_height)
|
||||||
.padding(0)
|
.padding(0)
|
||||||
@@ -1691,7 +1700,7 @@ impl FereseShell {
|
|||||||
|
|
||||||
let selector = motion::button(
|
let selector = motion::button(
|
||||||
workspace_button,
|
workspace_button,
|
||||||
color(if workspace.active {
|
color(if active {
|
||||||
shell_theme.accent
|
shell_theme.accent
|
||||||
} else {
|
} else {
|
||||||
shell_theme.text_primary
|
shell_theme.text_primary
|
||||||
@@ -1702,8 +1711,22 @@ impl FereseShell {
|
|||||||
// Iced tooltips are overlays inside this bar-height layer surface;
|
// Iced tooltips are overlays inside this bar-height layer surface;
|
||||||
// viewport clamping puts them back over the selector. Keep the
|
// viewport clamping puts them back over the selector. Keep the
|
||||||
// accessible button name, without an overlay stealing its target.
|
// accessible button name, without an overlay stealing its target.
|
||||||
workspace_row = workspace_row.push(selector);
|
workspace_buttons = workspace_buttons.push(selector);
|
||||||
}
|
}
|
||||||
|
workspace_row = workspace_row.push(container(workspace_buttons).padding([0, 3]).class(
|
||||||
|
theme::Container::custom(move |_| container::Style {
|
||||||
|
background: Some(Background::Color(color_with_opacity(
|
||||||
|
shell_theme.text_primary,
|
||||||
|
0.035,
|
||||||
|
))),
|
||||||
|
border: Border {
|
||||||
|
color: color_with_opacity(shell_theme.text_muted, 0.12),
|
||||||
|
width: 1.0,
|
||||||
|
radius: 7.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
let foreground = color(shell_theme.text_primary);
|
let foreground = color(shell_theme.text_primary);
|
||||||
let left = cosmic::iced::widget::scrollable(workspace_row)
|
let left = cosmic::iced::widget::scrollable(workspace_row)
|
||||||
@@ -1865,49 +1888,63 @@ fn output_bar_hidden(snapshot: &ShellSnapshot, output: Option<u64>) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn workspace_indicator(
|
fn workspace_indicator(
|
||||||
|
name: &str,
|
||||||
active: bool,
|
active: bool,
|
||||||
|
active_elsewhere: bool,
|
||||||
occupied: bool,
|
occupied: bool,
|
||||||
bar: BarMetrics,
|
bar: BarMetrics,
|
||||||
shell_theme: ShellTheme,
|
shell_theme: ShellTheme,
|
||||||
) -> Element<'static, cosmic::Action<Message>> {
|
) -> Element<'static, cosmic::Action<Message>> {
|
||||||
let marker = container(text(""))
|
let foreground = if active {
|
||||||
.width(if active { 18.0 } else { 6.0 })
|
shell_theme.accent
|
||||||
.height(6)
|
} else if occupied {
|
||||||
.class(theme::Container::custom(move |_| {
|
shell_theme.text_primary
|
||||||
workspace_selector_style(active, occupied, shell_theme)
|
} else {
|
||||||
}));
|
shell_theme.text_muted
|
||||||
container(marker)
|
};
|
||||||
.width(28)
|
container(
|
||||||
.height(bar.control_height)
|
text(name.to_owned())
|
||||||
.align_x(alignment::Horizontal::Center)
|
.size(12)
|
||||||
.align_y(alignment::Vertical::Center)
|
.class(theme::Text::Color(color(foreground))),
|
||||||
.into()
|
)
|
||||||
|
.width(24)
|
||||||
|
.height(bar.control_height)
|
||||||
|
.align_x(alignment::Horizontal::Center)
|
||||||
|
.align_y(alignment::Vertical::Center)
|
||||||
|
.class(theme::Container::custom(move |_| {
|
||||||
|
workspace_selector_style(active, active_elsewhere, occupied, shell_theme)
|
||||||
|
}))
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workspace_on_bar(workspace_output: Option<u64>, bar_output: Option<u64>) -> bool {
|
fn workspace_active_on_bar(workspace: u64, output: Option<&control::OutputSnapshot>) -> bool {
|
||||||
matches!((workspace_output, bar_output), (Some(workspace), Some(bar)) if workspace == bar)
|
output.is_some_and(|output| output.active_workspace == workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workspace_selector_style(
|
fn workspace_selector_style(
|
||||||
active: bool,
|
active: bool,
|
||||||
|
active_elsewhere: bool,
|
||||||
occupied: bool,
|
occupied: bool,
|
||||||
shell_theme: ShellTheme,
|
shell_theme: ShellTheme,
|
||||||
) -> container::Style {
|
) -> container::Style {
|
||||||
container::Style {
|
container::Style {
|
||||||
background: if active {
|
background: if active {
|
||||||
Some(Background::Color(color(shell_theme.accent)))
|
Some(Background::Color(color_with_opacity(
|
||||||
|
shell_theme.accent,
|
||||||
|
0.16,
|
||||||
|
)))
|
||||||
} else if occupied {
|
} else if occupied {
|
||||||
Some(Background::Color(color_with_opacity(
|
Some(Background::Color(color_with_opacity(
|
||||||
shell_theme.text_primary,
|
shell_theme.text_primary,
|
||||||
0.78,
|
0.04,
|
||||||
)))
|
)))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
border: Border {
|
border: Border {
|
||||||
color: color_with_opacity(shell_theme.text_muted, 0.55),
|
color: color_with_opacity(shell_theme.accent, 0.55),
|
||||||
width: if !active && !occupied { 1.0 } else { 0.0 },
|
width: if active_elsewhere { 1.0 } else { 0.0 },
|
||||||
radius: 3.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@@ -2434,25 +2471,33 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn workspace_selector_has_distinct_active_paint() {
|
fn workspace_selector_has_distinct_active_paint() {
|
||||||
let theme = ShellTheme::default();
|
let theme = ShellTheme::default();
|
||||||
let active = workspace_selector_style(true, false, theme);
|
let active = workspace_selector_style(true, false, false, theme);
|
||||||
let occupied = workspace_selector_style(false, true, theme);
|
let occupied = workspace_selector_style(false, false, true, theme);
|
||||||
let inactive = workspace_selector_style(false, false, theme);
|
let inactive = workspace_selector_style(false, false, false, theme);
|
||||||
|
let remote = workspace_selector_style(false, true, false, theme);
|
||||||
assert!(active.background.is_some());
|
assert!(active.background.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
active.background,
|
active.background,
|
||||||
Some(Background::Color(color(theme.accent)))
|
Some(Background::Color(color_with_opacity(theme.accent, 0.16)))
|
||||||
);
|
);
|
||||||
assert!(occupied.background.is_some());
|
assert!(occupied.background.is_some());
|
||||||
assert!(inactive.background.is_none());
|
assert!(inactive.background.is_none());
|
||||||
assert_eq!(inactive.border.width, 1.0);
|
assert_eq!(inactive.border.width, 0.0);
|
||||||
|
assert_eq!(remote.border.width, 1.0);
|
||||||
|
assert!(remote.background.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn workspace_selector_is_output_local() {
|
fn workspace_highlight_tracks_the_bars_output() {
|
||||||
assert!(workspace_on_bar(Some(1), Some(1)));
|
let output = control::OutputSnapshot {
|
||||||
assert!(!workspace_on_bar(Some(2), Some(1)));
|
id: 1,
|
||||||
assert!(!workspace_on_bar(None, Some(1)));
|
name: "eDP-1".into(),
|
||||||
assert!(!workspace_on_bar(None, None));
|
active_workspace: 2,
|
||||||
|
focused: false,
|
||||||
|
};
|
||||||
|
assert!(workspace_active_on_bar(2, Some(&output)));
|
||||||
|
assert!(!workspace_active_on_bar(1, Some(&output)));
|
||||||
|
assert!(!workspace_active_on_bar(2, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user