Fix screenshots and workspace switching; add configurable swipe bindings

This commit is contained in:
Samuel Onoja committed 2026-09-27 05:21:52 +01:00
1 parent 5d6c610fec
commit dbdfc5688e
14 files changed
+707 -101

No files matched your search

+40 -3
View File
@@ -67,6 +67,7 @@ enum Message {
WallpaperPicked(Result<Option<String>, String>),
NewCommand(String),
AddCommand,
AddSwipe(&'static str),
Remove(String, usize),
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 => {
self.inputs.clear();
let task = self.change(Edit::Remove(table, index));
@@ -705,7 +722,27 @@ impl cosmic::Application for App {
);
}
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") {
let prefix = format!("bindings.{index}");
let group = column([])
@@ -713,13 +750,13 @@ impl cosmic::Application for App {
.push(self.field(schema::text(
format!("{prefix}.keys"),
"Shortcut",
"For example: Super+Return",
"For example: Super+Return or Swipe3Up",
"",
)))
.push(self.field(schema::text(
format!("{prefix}.action"),
"Action",
"Ferese command, such as spawn or focus.",
"Action such as toggle-overview, spawn, focus, move, or none.",
"",
)))
.push(self.field(schema::text(
+11
View File
@@ -857,6 +857,17 @@ pub fn fields(page: Page) -> Vec<Field> {
"Avoid accidental pointer movement.",
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![],
}
+86 -41
View File
@@ -1649,7 +1649,6 @@ impl FereseShell {
}
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 bar = BarMetrics::from(shell_theme);
let mut workspace_row = row::with_capacity(self.snapshot.workspaces.len() + 1)
@@ -1665,23 +1664,33 @@ impl FereseShell {
self.overview_active,
1.0,
));
for workspace in self
.snapshot
.workspaces
.iter()
.filter(|workspace| workspace_on_bar(workspace.output, focused_output_id))
{
let mut workspace_buttons = row::with_capacity(self.snapshot.workspaces.len())
.spacing(1)
.align_y(cosmic::iced::Alignment::Center);
for workspace in &self.snapshot.workspaces {
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
.snapshot
.windows
.iter()
.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)
.name(format!(
"Workspace {}{}",
"Workspace {}{}{}",
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)
.padding(0)
@@ -1691,7 +1700,7 @@ impl FereseShell {
let selector = motion::button(
workspace_button,
color(if workspace.active {
color(if active {
shell_theme.accent
} else {
shell_theme.text_primary
@@ -1702,8 +1711,22 @@ impl FereseShell {
// Iced tooltips are overlays inside this bar-height layer surface;
// viewport clamping puts them back over the selector. Keep the
// 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 left = cosmic::iced::widget::scrollable(workspace_row)
@@ -1865,49 +1888,63 @@ fn output_bar_hidden(snapshot: &ShellSnapshot, output: Option<u64>) -> bool {
}
fn workspace_indicator(
name: &str,
active: bool,
active_elsewhere: bool,
occupied: bool,
bar: BarMetrics,
shell_theme: ShellTheme,
) -> Element<'static, cosmic::Action<Message>> {
let marker = container(text(""))
.width(if active { 18.0 } else { 6.0 })
.height(6)
.class(theme::Container::custom(move |_| {
workspace_selector_style(active, occupied, shell_theme)
}));
container(marker)
.width(28)
.height(bar.control_height)
.align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::Center)
.into()
let foreground = if active {
shell_theme.accent
} else if occupied {
shell_theme.text_primary
} else {
shell_theme.text_muted
};
container(
text(name.to_owned())
.size(12)
.class(theme::Text::Color(color(foreground))),
)
.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 {
matches!((workspace_output, bar_output), (Some(workspace), Some(bar)) if workspace == bar)
fn workspace_active_on_bar(workspace: u64, output: Option<&control::OutputSnapshot>) -> bool {
output.is_some_and(|output| output.active_workspace == workspace)
}
fn workspace_selector_style(
active: bool,
active_elsewhere: bool,
occupied: bool,
shell_theme: ShellTheme,
) -> container::Style {
container::Style {
background: if active {
Some(Background::Color(color(shell_theme.accent)))
Some(Background::Color(color_with_opacity(
shell_theme.accent,
0.16,
)))
} else if occupied {
Some(Background::Color(color_with_opacity(
shell_theme.text_primary,
0.78,
0.04,
)))
} else {
None
},
border: Border {
color: color_with_opacity(shell_theme.text_muted, 0.55),
width: if !active && !occupied { 1.0 } else { 0.0 },
radius: 3.0.into(),
color: color_with_opacity(shell_theme.accent, 0.55),
width: if active_elsewhere { 1.0 } else { 0.0 },
radius: 6.0.into(),
},
..Default::default()
}
@@ -2434,25 +2471,33 @@ mod tests {
#[test]
fn workspace_selector_has_distinct_active_paint() {
let theme = ShellTheme::default();
let active = workspace_selector_style(true, false, theme);
let occupied = workspace_selector_style(false, true, theme);
let inactive = workspace_selector_style(false, false, theme);
let active = workspace_selector_style(true, false, false, theme);
let occupied = workspace_selector_style(false, false, true, 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_eq!(
active.background,
Some(Background::Color(color(theme.accent)))
Some(Background::Color(color_with_opacity(theme.accent, 0.16)))
);
assert!(occupied.background.is_some());
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]
fn workspace_selector_is_output_local() {
assert!(workspace_on_bar(Some(1), Some(1)));
assert!(!workspace_on_bar(Some(2), Some(1)));
assert!(!workspace_on_bar(None, Some(1)));
assert!(!workspace_on_bar(None, None));
fn workspace_highlight_tracks_the_bars_output() {
let output = control::OutputSnapshot {
id: 1,
name: "eDP-1".into(),
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]