Center focused window titles and simplify Control Center controls
This commit is contained in:
1 parent
e9dcc968e1
commit
f933484c7b
5 files changed
+177
-68
No files matched your search
@@ -353,6 +353,7 @@ returns. Ferese does not change the system's suspend policy.
|
|||||||
|
|
||||||
| `status` key | Type | Default | Meaning |
|
| `status` key | Type | Default | Meaning |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
|
| `window_title` | boolean | `true` | Focused window title in the bar center when space allows |
|
||||||
| `battery_percentage` | boolean | `true` | Show percentage beside icon |
|
| `battery_percentage` | boolean | `true` | Show percentage beside icon |
|
||||||
| `low_battery_threshold` | integer 0–100 | `20` | Warning-color threshold |
|
| `low_battery_threshold` | integer 0–100 | `20` | Warning-color threshold |
|
||||||
| `settings_command` | argument array | `["ferese-settings"]` | Settings launcher; `[]` hides the action |
|
| `settings_command` | argument array | `["ferese-settings"]` | Settings launcher; `[]` hides the action |
|
||||||
|
|||||||
@@ -705,6 +705,12 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
Page::Bar => vec![
|
Page::Bar => vec![
|
||||||
|
toggle(
|
||||||
|
"status.window_title",
|
||||||
|
"Focused window title",
|
||||||
|
"Show the focused window title in the center of the bar when space allows.",
|
||||||
|
true,
|
||||||
|
),
|
||||||
range(
|
range(
|
||||||
"theme.geometry.top_bar_height",
|
"theme.geometry.top_bar_height",
|
||||||
"Height",
|
"Height",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub(crate) struct ShellConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) struct StatusConfig {
|
pub(crate) struct StatusConfig {
|
||||||
pub(crate) battery_percentage: bool,
|
pub(crate) battery_percentage: bool,
|
||||||
|
pub(crate) window_title: bool,
|
||||||
pub(crate) low_battery_threshold: u8,
|
pub(crate) low_battery_threshold: u8,
|
||||||
pub(crate) settings_command: Option<Vec<String>>,
|
pub(crate) settings_command: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
@@ -28,6 +29,7 @@ impl Default for StatusConfig {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
battery_percentage: true,
|
battery_percentage: true,
|
||||||
|
window_title: true,
|
||||||
low_battery_threshold: 20,
|
low_battery_threshold: 20,
|
||||||
settings_command: Some(vec!["ferese-settings".into()]),
|
settings_command: Some(vec!["ferese-settings".into()]),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1229,7 +1229,6 @@ impl FereseShell {
|
|||||||
size_limits: Limits::NONE,
|
size_limits: Limits::NONE,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|
||||||
Some(Box::new(Self::view_note_drag)),
|
Some(Box::new(Self::view_note_drag)),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1360,7 +1359,6 @@ impl FereseShell {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
Some(Box::new(move |app| app.view_note(¬e_id, id))),
|
Some(Box::new(move |app| app.view_note(¬e_id, id))),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1586,7 +1584,6 @@ impl FereseShell {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
Some(Box::new(move |app| app.view_desktop_clock(id))),
|
Some(Box::new(move |app| app.view_desktop_clock(id))),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2199,11 +2196,47 @@ impl FereseShell {
|
|||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.align_y(cosmic::iced::Alignment::Center);
|
.align_y(cosmic::iced::Alignment::Center);
|
||||||
|
let available = self
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.find(|output| output.bar == id)
|
||||||
|
.and_then(|output| output.size)
|
||||||
|
.map_or(0., |(width, _)| width as f32)
|
||||||
|
- 2. * (shell_theme.panel_padding + shell_theme.bar_margin_horizontal as f32);
|
||||||
|
let title_width = (available - 920.).clamp(0., 360.);
|
||||||
|
let title = if self.config.status.window_title && title_width > 0. {
|
||||||
|
focused_bar_title(&self.snapshot, focused_output)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
let center = text(title)
|
||||||
|
.size(bar.text_size)
|
||||||
|
.width(title_width)
|
||||||
|
.height(bar.control_height)
|
||||||
|
.align_x(alignment::Horizontal::Center)
|
||||||
|
.align_y(alignment::Vertical::Center)
|
||||||
|
.wrapping(cosmic::iced::widget::text::Wrapping::None)
|
||||||
|
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
||||||
|
cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(1),
|
||||||
|
))
|
||||||
|
.class(theme::Text::Color(foreground));
|
||||||
|
let right = cosmic::iced::widget::scrollable(container(right).width(Length::Shrink))
|
||||||
|
.direction(cosmic::iced::widget::scrollable::Direction::Horizontal(
|
||||||
|
cosmic::iced::widget::scrollable::Scrollbar::default()
|
||||||
|
.width(0)
|
||||||
|
.scroller_width(0),
|
||||||
|
))
|
||||||
|
.anchor_right()
|
||||||
|
.width(Length::Shrink)
|
||||||
|
.height(bar.control_height);
|
||||||
let content = row![
|
let content = row![
|
||||||
container(left).width(Length::Fill),
|
container(left).width(Length::Fill),
|
||||||
container(right).width(Length::Shrink),
|
center,
|
||||||
|
container(right)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(alignment::Horizontal::Right),
|
||||||
]
|
]
|
||||||
.spacing(16)
|
.spacing(8)
|
||||||
.align_y(cosmic::iced::Alignment::Center)
|
.align_y(cosmic::iced::Alignment::Center)
|
||||||
.height(Length::Fill);
|
.height(Length::Fill);
|
||||||
|
|
||||||
@@ -2362,6 +2395,26 @@ fn workspace_indicator(
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn focused_bar_title<'a>(
|
||||||
|
snapshot: &'a ShellSnapshot,
|
||||||
|
output: Option<&control::OutputSnapshot>,
|
||||||
|
) -> &'a str {
|
||||||
|
let Some(output) = output else {
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
snapshot
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.find(|window| window.focused && window.workspace == output.active_workspace)
|
||||||
|
.map_or("", |window| {
|
||||||
|
if window.title.trim().is_empty() {
|
||||||
|
&window.app_id
|
||||||
|
} else {
|
||||||
|
&window.title
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn workspace_active_on_bar(workspace: u64, output: Option<&control::OutputSnapshot>) -> bool {
|
fn workspace_active_on_bar(workspace: u64, output: Option<&control::OutputSnapshot>) -> bool {
|
||||||
output.is_some_and(|output| output.active_workspace == workspace)
|
output.is_some_and(|output| output.active_workspace == workspace)
|
||||||
}
|
}
|
||||||
@@ -3072,6 +3125,27 @@ mod tests {
|
|||||||
assert!(!output_bar_hidden(&snapshot, Some(2)));
|
assert!(!output_bar_hidden(&snapshot, Some(2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bar_title_tracks_focus_and_the_outputs_active_workspace() {
|
||||||
|
let mut snapshot = snapshot_with_fullscreen_window(7);
|
||||||
|
assert_eq!(
|
||||||
|
focused_bar_title(&snapshot, snapshot.outputs.first()),
|
||||||
|
"Test"
|
||||||
|
);
|
||||||
|
snapshot.windows[0].focused = false;
|
||||||
|
assert_eq!(focused_bar_title(&snapshot, snapshot.outputs.first()), "");
|
||||||
|
snapshot.windows[0].focused = true;
|
||||||
|
snapshot.windows[0].workspace = 8;
|
||||||
|
assert_eq!(focused_bar_title(&snapshot, snapshot.outputs.first()), "");
|
||||||
|
snapshot.windows[0].workspace = 7;
|
||||||
|
snapshot.windows[0].title.clear();
|
||||||
|
assert_eq!(
|
||||||
|
focused_bar_title(&snapshot, snapshot.outputs.first()),
|
||||||
|
"dev.ferese.Test"
|
||||||
|
);
|
||||||
|
assert_eq!(focused_bar_title(&snapshot, None), "");
|
||||||
|
}
|
||||||
|
|
||||||
fn snapshot_with_fullscreen_window(workspace: u64) -> ShellSnapshot {
|
fn snapshot_with_fullscreen_window(workspace: u64) -> ShellSnapshot {
|
||||||
ShellSnapshot {
|
ShellSnapshot {
|
||||||
outputs: vec![OutputSnapshot {
|
outputs: vec![OutputSnapshot {
|
||||||
|
|||||||
@@ -652,8 +652,7 @@ impl FereseShell {
|
|||||||
}
|
}
|
||||||
if combined || menu.kind == Menu::Notifications {
|
if combined || menu.kind == Menu::Notifications {
|
||||||
if let Some(n) = &self.status.notifications {
|
if let Some(n) = &self.status.notifications {
|
||||||
let toggle =
|
let toggle = toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), theme, p);
|
||||||
toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), primary, p);
|
|
||||||
rows = rows.push(if combined {
|
rows = rows.push(if combined {
|
||||||
control_card(toggle, primary, p)
|
control_card(toggle, primary, p)
|
||||||
} else {
|
} else {
|
||||||
@@ -802,7 +801,6 @@ impl FereseShell {
|
|||||||
}
|
}
|
||||||
if combined {
|
if combined {
|
||||||
let mut actions = column::with_capacity(2).spacing(8);
|
let mut actions = column::with_capacity(2).spacing(8);
|
||||||
let mut power = row::with_capacity(3).spacing(6).width(Length::Fill);
|
|
||||||
if self
|
if self
|
||||||
.config
|
.config
|
||||||
.status
|
.status
|
||||||
@@ -818,33 +816,6 @@ impl FereseShell {
|
|||||||
p,
|
p,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if self.status.poweroff {
|
|
||||||
power = power.push(footer_button(
|
|
||||||
"Power off…",
|
|
||||||
Message::ConfirmPower(Action::Poweroff),
|
|
||||||
primary,
|
|
||||||
p,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if self.status.reboot {
|
|
||||||
power = power.push(footer_button(
|
|
||||||
"Restart…",
|
|
||||||
Message::ConfirmPower(Action::Reboot),
|
|
||||||
primary,
|
|
||||||
p,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if self.status.suspend {
|
|
||||||
power = power.push(footer_button(
|
|
||||||
"Suspend…",
|
|
||||||
Message::ConfirmPower(Action::Suspend),
|
|
||||||
primary,
|
|
||||||
p,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if self.status.poweroff || self.status.reboot || self.status.suspend {
|
|
||||||
actions = actions.push(power);
|
|
||||||
}
|
|
||||||
rows = rows.push(control_card(actions.into(), primary, p));
|
rows = rows.push(control_card(actions.into(), primary, p));
|
||||||
}
|
}
|
||||||
if !menu.kind.available(&self.status) {
|
if !menu.kind.available(&self.status) {
|
||||||
@@ -1022,33 +993,23 @@ fn connection_control<'a>(
|
|||||||
source: &'static [u8],
|
source: &'static [u8],
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
action: Option<Action>,
|
action: Option<Action>,
|
||||||
theme: ShellTheme,
|
palette: ShellTheme,
|
||||||
opacity: f32,
|
opacity: f32,
|
||||||
) -> Element<'a, cosmic::Action<Message>> {
|
) -> Element<'a, cosmic::Action<Message>> {
|
||||||
let foreground = color_with_opacity(theme.text_primary, opacity);
|
let foreground = color_with_opacity(palette.text_primary, opacity);
|
||||||
let icon_accent = color_with_opacity(theme.accent, opacity);
|
let icon_color = color_with_opacity(
|
||||||
let accent = color_with_opacity(theme.accent, opacity * 0.14);
|
if enabled {
|
||||||
let icon = container(accented_icon(source, 18, foreground, icon_accent))
|
palette.accent
|
||||||
|
} else {
|
||||||
|
palette.text_muted
|
||||||
|
},
|
||||||
|
opacity,
|
||||||
|
);
|
||||||
|
let icon = container(accented_icon(source, 18, icon_color, icon_color))
|
||||||
.width(32)
|
.width(32)
|
||||||
.height(32)
|
.height(32)
|
||||||
.align_x(alignment::Horizontal::Center)
|
.align_x(alignment::Horizontal::Center)
|
||||||
.align_y(alignment::Vertical::Center)
|
.align_y(alignment::Vertical::Center);
|
||||||
.class(theme::Container::custom(move |_| container::Style {
|
|
||||||
background: enabled.then_some(Background::Color(accent)),
|
|
||||||
icon_color: Some(color_with_opacity(
|
|
||||||
if enabled {
|
|
||||||
theme.text_primary
|
|
||||||
} else {
|
|
||||||
theme.text_muted
|
|
||||||
},
|
|
||||||
opacity,
|
|
||||||
)),
|
|
||||||
border: Border {
|
|
||||||
radius: motion::radius(20.0).into(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}));
|
|
||||||
let content = column![
|
let content = column![
|
||||||
icon,
|
icon,
|
||||||
text(label).size(13),
|
text(label).size(13),
|
||||||
@@ -1058,14 +1019,12 @@ fn connection_control<'a>(
|
|||||||
.height(18)
|
.height(18)
|
||||||
.wrapping(cosmic::iced::widget::text::Wrapping::None)
|
.wrapping(cosmic::iced::widget::text::Wrapping::None)
|
||||||
.class(theme::Text::Color(color_with_opacity(
|
.class(theme::Text::Color(color_with_opacity(
|
||||||
theme.text_muted,
|
palette.text_muted,
|
||||||
opacity
|
opacity
|
||||||
)))
|
)))
|
||||||
]
|
]
|
||||||
.spacing(6)
|
.spacing(6)
|
||||||
.width(Length::Fill);
|
.width(Length::Fill);
|
||||||
// Battery is informational, not a toggle. Match its tile's layout without
|
|
||||||
// giving it a misleading clickable/disabled-button appearance.
|
|
||||||
let Some(action) = action else {
|
let Some(action) = action else {
|
||||||
return container(content)
|
return container(content)
|
||||||
.width(Length::FillPortion(1))
|
.width(Length::FillPortion(1))
|
||||||
@@ -1074,8 +1033,9 @@ fn connection_control<'a>(
|
|||||||
};
|
};
|
||||||
container(motion::button(
|
container(motion::button(
|
||||||
button::custom(content)
|
button::custom(content)
|
||||||
.width(Length::FillPortion(1))
|
.width(Length::Fill)
|
||||||
.padding(4)
|
.padding(4)
|
||||||
|
.name(format!("{label}: {}", if enabled { "on" } else { "off" }))
|
||||||
.on_press(cosmic::Action::App(Message::Control(action))),
|
.on_press(cosmic::Action::App(Message::Control(action))),
|
||||||
foreground,
|
foreground,
|
||||||
false,
|
false,
|
||||||
@@ -1267,21 +1227,87 @@ fn footer_button<'a>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shell_switch<'a>(
|
||||||
|
label: &str,
|
||||||
|
enabled: bool,
|
||||||
|
action: Action,
|
||||||
|
palette: ShellTheme,
|
||||||
|
opacity: f32,
|
||||||
|
) -> Element<'a, cosmic::Action<Message>> {
|
||||||
|
let track = if enabled {
|
||||||
|
color(palette.accent)
|
||||||
|
} else {
|
||||||
|
Color {
|
||||||
|
a: 0.22,
|
||||||
|
..color(palette.text_primary)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let thumb = if enabled {
|
||||||
|
ferese_theme::foreground(track, color(palette.text_primary))
|
||||||
|
} else {
|
||||||
|
color(palette.text_primary)
|
||||||
|
};
|
||||||
|
let knob = container(text(""))
|
||||||
|
.width(16)
|
||||||
|
.height(16)
|
||||||
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
|
background: Some(
|
||||||
|
Color {
|
||||||
|
a: opacity,
|
||||||
|
..thumb
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
border: Border {
|
||||||
|
radius: motion::radius(8.).into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
let toggle = container(knob)
|
||||||
|
.width(36)
|
||||||
|
.height(20)
|
||||||
|
.padding(2)
|
||||||
|
.align_x(if enabled {
|
||||||
|
alignment::Horizontal::Right
|
||||||
|
} else {
|
||||||
|
alignment::Horizontal::Left
|
||||||
|
})
|
||||||
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
|
background: Some(
|
||||||
|
Color {
|
||||||
|
a: track.a * opacity,
|
||||||
|
..track
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
border: Border {
|
||||||
|
radius: motion::radius(10.).into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
motion::button(
|
||||||
|
button::custom(toggle)
|
||||||
|
.padding(4)
|
||||||
|
.name(format!("{label}: {}", if enabled { "on" } else { "off" }))
|
||||||
|
.on_press(cosmic::Action::App(Message::Control(action))),
|
||||||
|
color(palette.text_primary),
|
||||||
|
false,
|
||||||
|
opacity,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn toggle_row<'a>(
|
fn toggle_row<'a>(
|
||||||
label: &'a str,
|
label: &'a str,
|
||||||
on: bool,
|
on: bool,
|
||||||
action: Action,
|
action: Action,
|
||||||
foreground: Color,
|
palette: ShellTheme,
|
||||||
opacity: f32,
|
opacity: f32,
|
||||||
) -> Element<'a, cosmic::Action<Message>> {
|
) -> Element<'a, cosmic::Action<Message>> {
|
||||||
row![
|
row![
|
||||||
text(label).width(Length::Fill).size(14),
|
text(label).width(Length::Fill).size(14),
|
||||||
menu_button(
|
shell_switch(label, on, action, palette, opacity)
|
||||||
if on { "On" } else { "Off" },
|
|
||||||
Message::Control(action),
|
|
||||||
foreground,
|
|
||||||
opacity
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
.align_y(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
.into()
|
.into()
|
||||||
|
|||||||
Reference in new issue
Block a user