feat(settings): add grouped configuration hub, languages and tray controls
This commit is contained in:
1 parent
6f34fd080f
commit
cc3b6f903f
90 files changed
+3625
-408
No files matched your search
@@ -30,6 +30,8 @@ fn main() -> iced::Result {
|
||||
ferese_config::config_path()
|
||||
};
|
||||
|
||||
ferese_config::locale::load(config.as_deref());
|
||||
|
||||
if !args.is_empty() && !preview && args != ["--foreground"] {
|
||||
eprintln!("Usage: ferese-lock [--preview | --foreground]");
|
||||
std::process::exit(2);
|
||||
@@ -430,7 +432,7 @@ impl Locker {
|
||||
let margin = if size.width < 640. { 24. } else { 36. };
|
||||
let top = (size.height * 0.09).max(24.);
|
||||
let a = &self.appearance;
|
||||
let mut input = widget::text_input("Password", self.password.as_str())
|
||||
let mut input = widget::text_input(ferese_config::tr!("Password"), self.password.as_str())
|
||||
.password()
|
||||
.padding([4, 10])
|
||||
.size(14)
|
||||
@@ -494,9 +496,9 @@ impl Locker {
|
||||
input,
|
||||
self.label(
|
||||
if self.caps_lock {
|
||||
"Caps Lock is on"
|
||||
ferese_config::tr!("Caps Lock is on")
|
||||
} else {
|
||||
self.status
|
||||
ferese_config::locale::text(self.status)
|
||||
},
|
||||
12
|
||||
)
|
||||
@@ -542,9 +544,12 @@ impl Locker {
|
||||
container(identity).center_x(Length::Fill),
|
||||
widget::Space::new().height(Length::Fill),
|
||||
row![
|
||||
row![ferese_symbol(), self.label("Ferese", 13)]
|
||||
.spacing(5)
|
||||
.align_y(iced::Alignment::Center),
|
||||
row![
|
||||
ferese_symbol(),
|
||||
self.label(ferese_config::tr!("Ferese"), 13)
|
||||
]
|
||||
.spacing(5)
|
||||
.align_y(iced::Alignment::Center),
|
||||
widget::Space::new().width(Length::Fill),
|
||||
self.label(
|
||||
if self.preview {
|
||||
|
||||
@@ -324,6 +324,7 @@ async fn run_agent() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
ferese_config::locale::load(ferese_config::config_path().as_deref());
|
||||
// SAFETY: PR_SET_DUMPABLE takes an integer value and does not dereference caller memory.
|
||||
unsafe {
|
||||
libc::prctl(libc::PR_SET_DUMPABLE, 0);
|
||||
|
||||
@@ -475,13 +475,15 @@ impl cosmic::Application for Prompt {
|
||||
Message::Submit
|
||||
});
|
||||
}
|
||||
let action = button::custom(ferese_theme::text("Authenticate", palette.font).size(13))
|
||||
.class(ferese_theme::controls::authentication_button(
|
||||
palette.palette(),
|
||||
))
|
||||
.height(Length::Fixed(36.))
|
||||
.padding([8, 16])
|
||||
.on_press_maybe((!self.waiting && !self.answer.is_empty()).then_some(Message::Submit));
|
||||
let action = button::custom(
|
||||
ferese_theme::text(ferese_config::tr!("Authenticate"), palette.font).size(13),
|
||||
)
|
||||
.class(ferese_theme::controls::authentication_button(
|
||||
palette.palette(),
|
||||
))
|
||||
.height(Length::Fixed(36.))
|
||||
.padding([8, 16])
|
||||
.on_press_maybe((!self.waiting && !self.answer.is_empty()).then_some(Message::Submit));
|
||||
let lock_icon = if let Some(icon) = &self.action_icon {
|
||||
widget::icon(icon.clone()).size(20)
|
||||
} else {
|
||||
@@ -506,7 +508,8 @@ impl cosmic::Application for Prompt {
|
||||
12.,
|
||||
)),
|
||||
column![
|
||||
ferese_theme::text("Authentication required", palette.font).size(18),
|
||||
ferese_theme::text(ferese_config::tr!("Authentication required"), palette.font)
|
||||
.size(18),
|
||||
ferese_theme::text(format!("Confirm as {user}"), palette.font)
|
||||
.size(12)
|
||||
.class(theme::Text::Color(palette.muted)),
|
||||
@@ -524,9 +527,9 @@ impl cosmic::Application for Prompt {
|
||||
button::custom(
|
||||
ferese_theme::text(
|
||||
if self.details_expanded {
|
||||
"Hide details"
|
||||
ferese_config::tr!("Hide details")
|
||||
} else {
|
||||
"Show details"
|
||||
ferese_config::tr!("Show details")
|
||||
},
|
||||
palette.font,
|
||||
)
|
||||
@@ -563,7 +566,7 @@ impl cosmic::Application for Prompt {
|
||||
body = body.push(
|
||||
container(
|
||||
row![
|
||||
ferese_theme::text("Account", palette.font)
|
||||
ferese_theme::text(ferese_config::tr!("Account"), palette.font)
|
||||
.size(12)
|
||||
.class(theme::Text::Color(palette.muted)),
|
||||
widget::dropdown(
|
||||
@@ -610,14 +613,16 @@ impl cosmic::Application for Prompt {
|
||||
}
|
||||
body = body.push(
|
||||
row![
|
||||
button::custom(ferese_theme::text("Cancel", palette.font).size(13))
|
||||
.class(ferese_theme::controls::material_button_style(
|
||||
palette.palette(),
|
||||
false,
|
||||
material_opacity,
|
||||
))
|
||||
.padding([8, 14])
|
||||
.on_press(Message::Cancel),
|
||||
button::custom(
|
||||
ferese_theme::text(ferese_config::tr!("Cancel"), palette.font).size(13)
|
||||
)
|
||||
.class(ferese_theme::controls::material_button_style(
|
||||
palette.palette(),
|
||||
false,
|
||||
material_opacity,
|
||||
))
|
||||
.padding([8, 14])
|
||||
.on_press(Message::Cancel),
|
||||
action
|
||||
]
|
||||
.spacing(8)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -64,7 +64,7 @@ impl App {
|
||||
)
|
||||
.push(
|
||||
ferese_theme::controls::switch(adapter.powered, p)
|
||||
.name("Bluetooth power")
|
||||
.name(ferese_config::tr!("Bluetooth power"))
|
||||
.on_press_maybe(ready.then(|| {
|
||||
message(Input::Run(Command::BluetoothPower(
|
||||
adapter.path.clone(),
|
||||
|
||||
@@ -49,7 +49,7 @@ impl App {
|
||||
)
|
||||
.push(
|
||||
ferese_theme::controls::switch(wifi.enabled, p)
|
||||
.name("Wi-Fi power")
|
||||
.name(ferese_config::tr!("Wi-Fi power"))
|
||||
.on_press_maybe(
|
||||
(ready
|
||||
&& (wifi.hardware_enabled || wifi.enabled)
|
||||
|
||||
@@ -36,14 +36,14 @@ impl App {
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(self.settings_icon_button(
|
||||
"Refresh displays",
|
||||
ferese_config::tr!("Refresh displays"),
|
||||
"M20 7V3l-3 3a8 8 0 1 0 3 6 M20 3h-5",
|
||||
Some(Message::RefreshDisplays),
|
||||
)),
|
||||
);
|
||||
if let Some(error) = &self.display_error {
|
||||
panel = panel.push(cosmic::widget::tooltip(
|
||||
self.note("Live display information unavailable"),
|
||||
self.note(ferese_config::tr!("Live display information unavailable")),
|
||||
self.label(error.clone(), 11.),
|
||||
cosmic::widget::tooltip::Position::Bottom,
|
||||
));
|
||||
@@ -159,13 +159,16 @@ impl App {
|
||||
)));
|
||||
panel = panel.push(row([]).spacing(5).align_y(Alignment::Center)
|
||||
.push(cosmic::widget::tooltip(visuals::action_icon("M12 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18 M12 11v6 M12 7h.01", palette.muted),
|
||||
self.label("Auto uses 60 Hz below 30% battery and returns to normal on AC or at 35%. Applying a display mode can briefly blank the screen.", 11.), cosmic::widget::tooltip::Position::Top))
|
||||
self.label(ferese_config::tr!("Auto uses 60 Hz below 30% battery and returns to normal on AC or at 35%. Applying a display mode can briefly blank the screen."), 11.), cosmic::widget::tooltip::Position::Top))
|
||||
.push(self.note(if target.active { "Changes apply live" } else { "Saved profile · applies when active" })));
|
||||
} else {
|
||||
panel = panel.push(self.note("Managed automatically · no saved display profile"));
|
||||
panel = panel.push(self.note(ferese_config::tr!(
|
||||
"Managed automatically · no saved display profile"
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
panel = panel.push(self.note("No displays or saved profiles found"));
|
||||
panel =
|
||||
panel.push(self.note(ferese_config::tr!("No displays or saved profiles found")));
|
||||
}
|
||||
container(panel)
|
||||
.padding([7, 10])
|
||||
|
||||
@@ -123,10 +123,10 @@ impl App {
|
||||
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||||
let mut labels = column([])
|
||||
.spacing(2)
|
||||
.push(self.label(field.label.clone(), 13.));
|
||||
.push(self.label(ferese_config::locale::translate(&field.label), 13.));
|
||||
if !field.description.is_empty() {
|
||||
labels = labels.push(
|
||||
self.label(field.description.clone(), 11.)
|
||||
self.label(ferese_config::locale::translate(&field.description), 11.)
|
||||
.class(cosmic::theme::Text::Color(palette.muted)),
|
||||
);
|
||||
}
|
||||
@@ -169,7 +169,7 @@ impl App {
|
||||
format!("{value:.2}{suffix}")
|
||||
};
|
||||
if margin_limit == Some(None) {
|
||||
self.label("—", 12.).into()
|
||||
self.label(ferese_config::tr!("—"), 12.).into()
|
||||
} else if max <= min {
|
||||
self.label(display, 12.).into()
|
||||
} else {
|
||||
@@ -195,7 +195,7 @@ impl App {
|
||||
let mut options = row([]).spacing(2);
|
||||
for (key, label) in choices {
|
||||
options = options.push(
|
||||
button::custom(self.label(*label, 12.))
|
||||
button::custom(self.label(ferese_config::locale::text(label), 12.))
|
||||
.padding([4, 8])
|
||||
.class(self.control_button_style(palette, value == *key))
|
||||
.on_press(Message::Change(set(&path, *key))),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod all_options;
|
||||
mod connections;
|
||||
mod form_controls;
|
||||
mod navigation;
|
||||
@@ -8,6 +9,7 @@ mod panel_edit;
|
||||
mod panel_inspector;
|
||||
mod panel_preview;
|
||||
mod theme_controls;
|
||||
mod tray_controls;
|
||||
|
||||
use theme_controls::gallery_id;
|
||||
|
||||
@@ -62,6 +64,11 @@ fn main() -> cosmic::iced::Result {
|
||||
}
|
||||
Some("wallpaper") => Some(InitialPage::Wallpaper),
|
||||
Some("bar") => Some(InitialPage::Bar),
|
||||
Some("appearance") => Some(InitialPage::Settings(Page::Appearance)),
|
||||
Some("general") => Some(InitialPage::Settings(Page::General)),
|
||||
Some("theme") => Some(InitialPage::Settings(Page::Theme)),
|
||||
Some("tray") => Some(InitialPage::Settings(Page::Tray)),
|
||||
Some("all-options") => Some(InitialPage::Settings(Page::Configuration)),
|
||||
_ => {
|
||||
eprintln!("--page accepts connections, wifi, bluetooth, wallpaper, or bar");
|
||||
return Ok(());
|
||||
@@ -79,6 +86,9 @@ fn main() -> cosmic::iced::Result {
|
||||
let path = path.canonicalize().unwrap_or(path);
|
||||
fonts::families();
|
||||
let initial = Snapshot::read(&path);
|
||||
if let Ok(snapshot) = &initial {
|
||||
ferese_config::locale::set(ferese_config::locale::from_document(&snapshot.doc));
|
||||
}
|
||||
let theme = visuals::native_theme(initial.as_ref().ok());
|
||||
|
||||
cosmic::app::run::<App>(
|
||||
@@ -94,12 +104,16 @@ fn main() -> cosmic::iced::Result {
|
||||
#[derive(Clone, Copy)]
|
||||
enum InitialPage {
|
||||
Connections(connections::Tab),
|
||||
Settings(Page),
|
||||
Wallpaper,
|
||||
Bar,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Message {
|
||||
Options(all_options::Input),
|
||||
TrayApps(Vec<tray_controls::Application>),
|
||||
RefreshTrayApps,
|
||||
ReadRenderScale(cosmic::iced::window::Id),
|
||||
RenderScale(f32),
|
||||
Connections(connections::Input),
|
||||
@@ -165,6 +179,8 @@ enum Message {
|
||||
}
|
||||
|
||||
struct App {
|
||||
tray_apps: Vec<tray_controls::Application>,
|
||||
options: all_options::State,
|
||||
connections: connections::State,
|
||||
displays: Vec<displays::Display>,
|
||||
display_selection: Option<String>,
|
||||
@@ -265,7 +281,11 @@ impl cosmic::Application for App {
|
||||
let font = fonts::interface_font(&resolved.presented.tokens.typography.font_family);
|
||||
let native_palette = visuals::Palette::from_resolved(&resolved.presented);
|
||||
let family_sections = theme_controls::FamilySections::new(&resolved.families, ¤t);
|
||||
ferese_config::locale::set(ferese_config::locale::from_document(¤t.doc));
|
||||
let options = all_options::State::load(¤t);
|
||||
let mut app = Self {
|
||||
tray_apps: Vec::new(),
|
||||
options,
|
||||
connections: connections::State {
|
||||
tab: match initial_page {
|
||||
Some(InitialPage::Connections(tab)) => tab,
|
||||
@@ -291,6 +311,7 @@ impl cosmic::Application for App {
|
||||
Some(InitialPage::Connections(_)) => Page::Connections,
|
||||
Some(InitialPage::Wallpaper) => Page::Wallpaper,
|
||||
Some(InitialPage::Bar) => Page::Bar,
|
||||
Some(InitialPage::Settings(page)) => page,
|
||||
None => Page::Appearance,
|
||||
},
|
||||
profile_pages: std::env::var_os("FERESE_PROFILE_SETTINGS").is_some(),
|
||||
@@ -328,9 +349,15 @@ impl cosmic::Application for App {
|
||||
let task = app
|
||||
.core
|
||||
.main_window_id()
|
||||
.map(|id| app.set_window_title("Ferese Settings".into(), id))
|
||||
.map(|id| app.set_window_title(ferese_config::tr!("Ferese Settings").into(), id))
|
||||
.unwrap_or_else(Task::none);
|
||||
|
||||
let tray_apps = if app.page == Page::Tray {
|
||||
app.load_tray_apps()
|
||||
} else {
|
||||
Task::none()
|
||||
};
|
||||
let task = Task::batch([task, tray_apps]);
|
||||
let connections = if app.page == Page::Connections {
|
||||
app.refresh_connections()
|
||||
} else {
|
||||
@@ -347,6 +374,9 @@ impl cosmic::Application for App {
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::TrayApps(apps) => self.tray_apps = apps,
|
||||
Message::RefreshTrayApps => return self.load_tray_apps(),
|
||||
Message::Options(input) => return self.options_input(input),
|
||||
Message::DragWindow => return self.core.drag(None),
|
||||
Message::ReadRenderScale(id) => {
|
||||
return cosmic::iced::window::scale_factor(id)
|
||||
@@ -427,11 +457,16 @@ impl cosmic::Application for App {
|
||||
|| !self.inputs.is_empty()
|
||||
|| !self.ranges.is_empty()
|
||||
|| !self.pending.is_empty()
|
||||
|| self.options.dirty()
|
||||
{
|
||||
self.status =
|
||||
"Config changed externally · Reload when your edits are finished"
|
||||
.into();
|
||||
} else {
|
||||
ferese_config::locale::set(ferese_config::locale::from_document(
|
||||
&snapshot.doc,
|
||||
));
|
||||
self.options.reset(&snapshot);
|
||||
self.current = snapshot.clone();
|
||||
self.draft = snapshot;
|
||||
self.family_sections = theme_controls::FamilySections::new(
|
||||
@@ -493,6 +528,12 @@ impl cosmic::Application for App {
|
||||
|
||||
self.page_transition = self.profile_pages.then(std::time::Instant::now);
|
||||
let leaving = self.page == Page::Connections && page != Page::Connections;
|
||||
if page == Page::Configuration && !self.options.dirty() {
|
||||
self.options.reset(&self.current);
|
||||
}
|
||||
if page == Page::Configuration {
|
||||
self.options.filter = self.search.clone();
|
||||
}
|
||||
self.page = page;
|
||||
self.search.clear();
|
||||
self.visible_rows = initial_visible_rows();
|
||||
@@ -507,6 +548,7 @@ impl cosmic::Application for App {
|
||||
let load = match page {
|
||||
Page::Displays | Page::Bar => self.load_displays(),
|
||||
Page::Wallpaper => self.load_thumbnail(),
|
||||
Page::Tray => self.load_tray_apps(),
|
||||
_ => Task::none(),
|
||||
};
|
||||
return Task::batch([reset, cleanup, load]);
|
||||
@@ -517,6 +559,9 @@ impl cosmic::Application for App {
|
||||
if matches!(page, Page::Displays | Page::Bar) {
|
||||
return Task::batch([reset, self.load_displays()]);
|
||||
}
|
||||
if page == Page::Tray {
|
||||
return Task::batch([reset, self.load_tray_apps()]);
|
||||
}
|
||||
if page == Page::Wallpaper {
|
||||
return Task::batch([reset, self.load_thumbnail()]);
|
||||
}
|
||||
@@ -725,6 +770,12 @@ impl cosmic::Application for App {
|
||||
Ok((snapshot, live)) => {
|
||||
self.undo = Some(std::mem::take(&mut self.saving_previous));
|
||||
self.current = snapshot;
|
||||
ferese_config::locale::set(ferese_config::locale::from_document(
|
||||
&self.current.doc,
|
||||
));
|
||||
if !self.options.dirty() || self.options.applying {
|
||||
self.options.reset(&self.current);
|
||||
}
|
||||
self.font = fonts::interface_font(
|
||||
&self.resolved.presented.tokens.typography.font_family,
|
||||
);
|
||||
@@ -755,7 +806,18 @@ impl cosmic::Application for App {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
Message::ExpireUndo(revision)
|
||||
});
|
||||
let title = self
|
||||
.core
|
||||
.main_window_id()
|
||||
.map(|id| {
|
||||
self.set_window_title(
|
||||
ferese_config::tr!("Ferese Settings").into(),
|
||||
id,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(Task::none);
|
||||
return Task::batch([
|
||||
title,
|
||||
self.update_theme(),
|
||||
self.flush(),
|
||||
self.load_thumbnail(),
|
||||
@@ -766,12 +828,15 @@ impl cosmic::Application for App {
|
||||
self.error = Some(error);
|
||||
self.pending.clear();
|
||||
self.draft = self.current.clone();
|
||||
self.options.applying = false;
|
||||
self.status = "Not saved".into();
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Reload if !self.saving => match Snapshot::read(&self.path) {
|
||||
Ok(snapshot) => {
|
||||
ferese_config::locale::set(ferese_config::locale::from_document(&snapshot.doc));
|
||||
self.options.reset(&snapshot);
|
||||
self.current = snapshot.clone();
|
||||
self.draft = snapshot;
|
||||
self.family_sections =
|
||||
@@ -1754,7 +1819,7 @@ mod tests {
|
||||
.expect("requested renderer");
|
||||
eprintln!("renderer={}", Headless::name(&renderer));
|
||||
|
||||
for page in [Page::Shortcuts, Page::Appearance] {
|
||||
for page in [Page::Shortcuts, Page::Theme] {
|
||||
let mut app = shortcuts(500);
|
||||
app.page = page;
|
||||
let theme = app.native_palette.native_theme();
|
||||
|
||||
@@ -26,7 +26,7 @@ impl App {
|
||||
.align_y(Alignment::Center)
|
||||
.push(visuals::brand_icon())
|
||||
.push(
|
||||
self.label("Ferese", 16.)
|
||||
self.label(ferese_config::tr!("Ferese"), 16.)
|
||||
.class(cosmic::theme::Text::Color(palette.text)),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
@@ -36,7 +36,7 @@ impl App {
|
||||
)
|
||||
.push(widget::Space::new().height(14))
|
||||
.push(
|
||||
text_input("Search settings", self.search.clone())
|
||||
text_input(ferese_config::tr!("Search settings"), self.search.clone())
|
||||
.id(widget::Id::new("settings-search"))
|
||||
.on_input(Message::Search)
|
||||
.font(self.font)
|
||||
@@ -46,7 +46,7 @@ impl App {
|
||||
.push(widget::Space::new().height(8));
|
||||
if compact {
|
||||
sidebar = sidebar.push(self.settings_button(
|
||||
"Close navigation",
|
||||
ferese_config::tr!("Close navigation"),
|
||||
"M6 6l12 12 M6 18L18 6",
|
||||
Some(Message::Sidebar(false)),
|
||||
false,
|
||||
@@ -56,7 +56,7 @@ impl App {
|
||||
for (title, pages) in Page::NAVIGATION {
|
||||
let mut section = column([]).spacing(3).push(
|
||||
container(
|
||||
self.label(title, 11.)
|
||||
self.label(ferese_config::locale::text(title), 11.)
|
||||
.class(cosmic::theme::Text::Color(palette.muted)),
|
||||
)
|
||||
.padding([0, 8]),
|
||||
@@ -118,13 +118,13 @@ impl App {
|
||||
)
|
||||
.width(Length::Fill),
|
||||
);
|
||||
if self.page == Page::Appearance && self.search.is_empty() {
|
||||
if self.page == Page::Theme && self.search.is_empty() {
|
||||
let automatic = self.draft.string("theme.mode", "dark") == "auto";
|
||||
heading = heading.push(
|
||||
row([])
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center)
|
||||
.push(self.label("Auto", 12.))
|
||||
.push(self.label(ferese_config::tr!("Auto"), 12.))
|
||||
.push(
|
||||
ferese_theme::controls::switch(automatic, palette)
|
||||
.name(if automatic {
|
||||
@@ -179,11 +179,18 @@ impl App {
|
||||
.into_iter()
|
||||
.flat_map(|(_, pages)| pages.iter().copied())
|
||||
};
|
||||
if !pages().any(|p| p.matches(&self.search)) {
|
||||
body = body
|
||||
.push(self.note("No matching settings. Try wallpaper, keyboard, or motion."));
|
||||
if !pages().any(|p| {
|
||||
p.matches(&self.search)
|
||||
|| (p == Page::Configuration && self.options.matches(&self.search))
|
||||
}) {
|
||||
body = body.push(self.note(ferese_config::tr!(
|
||||
"No matching settings. Try wallpaper, keyboard, or motion."
|
||||
)));
|
||||
}
|
||||
for page in pages().filter(|p| p.matches(&self.search)) {
|
||||
for page in pages().filter(|p| {
|
||||
p.matches(&self.search)
|
||||
|| (*p == Page::Configuration && self.options.matches(&self.search))
|
||||
}) {
|
||||
let destination = page.search_destination(&self.search);
|
||||
body = body.push(
|
||||
button::custom(
|
||||
@@ -199,13 +206,24 @@ impl App {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if self.page == Page::Appearance {
|
||||
body = body.push(self.settings_hub());
|
||||
}
|
||||
if self.page == Page::Configuration {
|
||||
body = body.push(self.all_options_view());
|
||||
}
|
||||
if self.page == Page::Tray {
|
||||
body = body
|
||||
.push(self.tray_application_controls())
|
||||
.push(self.tray_status_controls());
|
||||
}
|
||||
if self.page == Page::Windows {
|
||||
body = body.push(visuals::preview_resolved(
|
||||
&self.draft,
|
||||
&self.resolved.presented,
|
||||
));
|
||||
}
|
||||
if self.page == Page::Appearance {
|
||||
if self.page == Page::Theme {
|
||||
let split = visuals::split(&self.draft);
|
||||
if split {
|
||||
body = body
|
||||
@@ -245,11 +263,11 @@ impl App {
|
||||
|
||||
if !fields.is_empty() {
|
||||
let mut group = column([]).spacing(1);
|
||||
if self.page == Page::Appearance && self.auto_details {
|
||||
if self.page == Page::Theme && self.auto_details {
|
||||
group = group.push(self.auto_controls());
|
||||
}
|
||||
for field in fields {
|
||||
if self.page == Page::Appearance
|
||||
if self.page == Page::Theme
|
||||
&& (field.path == "theme.mode"
|
||||
|| field.path.starts_with("theme.schedule.")
|
||||
|| field.path.ends_with(".file"))
|
||||
@@ -258,13 +276,13 @@ impl App {
|
||||
}
|
||||
group = group.push(self.field(field));
|
||||
}
|
||||
if self.page == Page::Appearance {
|
||||
if self.page == Page::Theme {
|
||||
group = group.push(
|
||||
button::custom(
|
||||
row([])
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center)
|
||||
.push(self.label("Advanced customization", 13.))
|
||||
.push(self.label(ferese_config::tr!("Advanced customization"), 13.))
|
||||
.push(visuals::action_icon(
|
||||
if self.advanced_theme {
|
||||
"M6 15l6-6 6 6"
|
||||
@@ -298,17 +316,17 @@ impl App {
|
||||
match self.page {
|
||||
Page::Connections => body = body.push(self.connections_view()),
|
||||
Page::LockScreen => {
|
||||
body = body.push(self.note("Uses your wallpaper, shell colors, font and corner radius from Appearance. Changes apply when the locker or preview opens."));
|
||||
body = body.push(self.note(ferese_config::tr!("Uses your wallpaper, shell colors, font and corner radius from Appearance. Changes apply when the locker or preview opens.")));
|
||||
body = body.push(self.settings_button(
|
||||
"Preview lock screen",
|
||||
ferese_config::tr!("Preview lock screen"),
|
||||
"M7 10V7a5 5 0 0 1 10 0v3 M5 10h14v11H5z M12 14v3",
|
||||
Some(Message::PreviewLock),
|
||||
false,
|
||||
));
|
||||
body = body.push(self.note("The preview is an ordinary window and does not lock your session. Automatic locking is configured separately in Login items."));
|
||||
body = body.push(self.note(ferese_config::tr!("The preview is an ordinary window and does not lock your session. Automatic locking is configured separately in Login items.")));
|
||||
}
|
||||
Page::Startup => {
|
||||
body = body.push(self.note("Login items update live. Disabling or removing an item stops the session-owned process; enabling one starts it."));
|
||||
body = body.push(self.note(ferese_config::tr!("Login items update live. Disabling or removing an item stops the session-owned process; enabling one starts it.")));
|
||||
|
||||
for index in 0..self.draft.records("autostart") {
|
||||
let prefix = format!("autostart.{index}");
|
||||
@@ -338,7 +356,7 @@ impl App {
|
||||
.push(
|
||||
container(
|
||||
self.settings_icon_button(
|
||||
"Remove login item",
|
||||
ferese_config::tr!("Remove login item"),
|
||||
"M4 6h16 M9 6V3h6v3 M6 6l1 15h10l1-15 M10 10v7 M14 10v7",
|
||||
(!self.saving)
|
||||
.then_some(Message::Remove("autostart".into(), index)),
|
||||
@@ -356,16 +374,19 @@ impl App {
|
||||
row([])
|
||||
.spacing(8)
|
||||
.push(
|
||||
text_input("Program and arguments", self.new_command.clone())
|
||||
.font(self.font)
|
||||
.size(12)
|
||||
.padding([4, 8])
|
||||
.style(visuals::input_style(palette))
|
||||
.on_input(Message::NewCommand),
|
||||
text_input(
|
||||
ferese_config::tr!("Program and arguments"),
|
||||
self.new_command.clone(),
|
||||
)
|
||||
.font(self.font)
|
||||
.size(12)
|
||||
.padding([4, 8])
|
||||
.style(visuals::input_style(palette))
|
||||
.on_input(Message::NewCommand),
|
||||
)
|
||||
.push(
|
||||
self.settings_button(
|
||||
"Add login item",
|
||||
ferese_config::tr!("Add login item"),
|
||||
"M12 5v14 M5 12h14",
|
||||
(!self.saving && !self.new_command.trim().is_empty())
|
||||
.then_some(Message::AddCommand),
|
||||
@@ -375,7 +396,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
Page::Shortcuts => {
|
||||
body = body.push(self.note("Keyboard, mouse and swipe bindings share the same actions. Mouse shortcuts can use MouseLeft, MouseRight, MouseMiddle, MouseBack, MouseForward or WheelUp/Down/Left/Right with modifiers. Use pointer-move or pointer-resize for dragging. Swipes use Swipe3Up, Swipe3Down, Swipe3Left or Swipe3Right (also supports 4 or 5 fingers). Actions such as toggle-overview and none need no argument."));
|
||||
body = body.push(self.note(ferese_config::tr!("Keyboard, mouse and swipe bindings share the same actions. Mouse shortcuts can use MouseLeft, MouseRight, MouseMiddle, MouseBack, MouseForward or WheelUp/Down/Left/Right with modifiers. Use pointer-move or pointer-resize for dragging. Swipes use Swipe3Up, Swipe3Down, Swipe3Left or Swipe3Right (also supports 4 or 5 fingers). Actions such as toggle-overview and none need no argument.")));
|
||||
let mut gestures = row([]).spacing(6);
|
||||
for (keys, label) in [
|
||||
("Swipe3Up", "Customize swipe up"),
|
||||
@@ -447,7 +468,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
if self.draft.records("bindings") == 0 {
|
||||
body = body.push(self.note("You are using the built-in shortcuts. Add custom bindings in config.kdl; they will appear here after Reload."));
|
||||
body = body.push(self.note(ferese_config::tr!("You are using the built-in shortcuts. Add custom bindings in config.kdl; they will appear here after Reload.")));
|
||||
}
|
||||
}
|
||||
Page::Displays => body = body.push(self.displays_view()),
|
||||
@@ -459,7 +480,7 @@ impl App {
|
||||
if compact {
|
||||
page_heading = page_heading.push(
|
||||
container(self.settings_icon_button(
|
||||
"Show settings navigation",
|
||||
ferese_config::tr!("Show settings navigation"),
|
||||
"M3 5h18v14H3z M9 5v14",
|
||||
Some(Message::Sidebar(true)),
|
||||
))
|
||||
@@ -478,7 +499,7 @@ impl App {
|
||||
content = content.push(
|
||||
container(
|
||||
row(self.page.subpages().iter().map(|&(page, title)| {
|
||||
button::custom(self.label(title, 13.))
|
||||
button::custom(self.label(ferese_config::locale::text(title), 13.))
|
||||
.name(format!("{}: {title}", self.page.navigation_page().title()))
|
||||
.padding([7, 12])
|
||||
.class(visuals::panel_button(palette, self.page == page))
|
||||
@@ -570,7 +591,7 @@ impl App {
|
||||
))
|
||||
.padding(4)
|
||||
.on_press_maybe((self.undo.is_some() && !self.saving).then_some(Message::Undo)),
|
||||
self.label("Undo last change", 11.),
|
||||
self.label(ferese_config::tr!("Undo last change"), 11.),
|
||||
widget::tooltip::Position::Top,
|
||||
))
|
||||
.push(widget::tooltip(
|
||||
@@ -580,11 +601,11 @@ impl App {
|
||||
))
|
||||
.padding(4)
|
||||
.on_press_maybe((!self.saving).then_some(Message::Reload)),
|
||||
self.label("Reload configuration", 11.),
|
||||
self.label(ferese_config::tr!("Reload configuration"), 11.),
|
||||
widget::tooltip::Position::Top,
|
||||
));
|
||||
let footer: Element<'_, Message> = if self.page == Page::Connections {
|
||||
self.note("Connections are managed by the system. Passwords are not saved in your Ferese config.")
|
||||
self.note(ferese_config::tr!("Connections are managed by the system. Passwords are not saved in your Ferese config."))
|
||||
} else if self.page == Page::Bar {
|
||||
container(
|
||||
row([])
|
||||
@@ -597,13 +618,13 @@ impl App {
|
||||
}))
|
||||
.push(widget::Space::new().width(Length::Fill))
|
||||
.push(self.settings_button(
|
||||
"Undo",
|
||||
ferese_config::tr!("Undo"),
|
||||
"M3 10h8 M3 10V3 M3 10c3-7 17-6 17 3a7 7 0 0 1-7 7",
|
||||
(self.undo.is_some() && !self.saving).then_some(Message::Undo),
|
||||
false,
|
||||
))
|
||||
.push(self.settings_button(
|
||||
"Reload configuration",
|
||||
ferese_config::tr!("Reload configuration"),
|
||||
"M21 3v6h-6 M3 21v-6h6 M3 9a9 9 0 0 1 15-6l3 6 M21 15a9 9 0 0 1-15 6l-3-6",
|
||||
(!self.saving).then_some(Message::Reload),
|
||||
false,
|
||||
|
||||
@@ -35,7 +35,7 @@ impl std::fmt::Display for StyleChoice {
|
||||
}
|
||||
}
|
||||
|
||||
const CATALOG: [CatalogItem; 15] = [
|
||||
const CATALOG: [CatalogItem; 16] = [
|
||||
CatalogItem(ItemKind::Overview),
|
||||
CatalogItem(ItemKind::Workspaces {
|
||||
style: ferese_panel::WorkspaceStyle::Dots,
|
||||
@@ -44,6 +44,7 @@ const CATALOG: [CatalogItem; 15] = [
|
||||
CatalogItem(ItemKind::Media),
|
||||
CatalogItem(ItemKind::Activities),
|
||||
CatalogItem(ItemKind::QuickSettings),
|
||||
CatalogItem(ItemKind::Tray),
|
||||
CatalogItem(ItemKind::NightLight),
|
||||
CatalogItem(ItemKind::Network),
|
||||
CatalogItem(ItemKind::Audio),
|
||||
@@ -58,7 +59,7 @@ const CATALOG: [CatalogItem; 15] = [
|
||||
impl App {
|
||||
pub(super) fn panel_navigation(&self) -> Element<'static, Message> {
|
||||
self.settings_button(
|
||||
"Back to Panels",
|
||||
ferese_config::tr!("Back to Panels"),
|
||||
"M15 18l-6-6 6-6",
|
||||
Some(Message::PanelPage(PanelPage::Panels)),
|
||||
false,
|
||||
@@ -71,52 +72,53 @@ impl App {
|
||||
let mut rows = widget::column([]).spacing(12);
|
||||
match self.panel_page {
|
||||
PanelPage::Panels => {
|
||||
rows =
|
||||
rows.push(
|
||||
widget::container(
|
||||
widget::row([])
|
||||
.spacing(12)
|
||||
.align_y(cosmic::iced::Alignment::Center)
|
||||
.push(crate::visuals::action_icon(
|
||||
if edge == Edge::Top {
|
||||
"M3 5h18v14H3z M3 9h18"
|
||||
} else {
|
||||
"M3 5h18v14H3z M3 15h18"
|
||||
},
|
||||
palette.accent,
|
||||
))
|
||||
.push(
|
||||
widget::column([])
|
||||
.spacing(3)
|
||||
.push(self.panel_heading(
|
||||
if edge == Edge::Top {
|
||||
"Top panel"
|
||||
} else {
|
||||
"Bottom panel"
|
||||
},
|
||||
14.,
|
||||
))
|
||||
.push(self.note("Shown on all displays"))
|
||||
.width(cosmic::iced::Length::Fill),
|
||||
)
|
||||
.push(self.settings_button(
|
||||
"Edit panel",
|
||||
"M4 20l4-1 12-12-3-3L5 16z",
|
||||
Some(Message::PanelPage(PanelPage::Arrange)),
|
||||
false,
|
||||
))
|
||||
.push(self.settings_icon_button(
|
||||
"Remove custom panel",
|
||||
rows = rows.push(
|
||||
widget::container(
|
||||
widget::row([])
|
||||
.spacing(12)
|
||||
.align_y(cosmic::iced::Alignment::Center)
|
||||
.push(crate::visuals::action_icon(
|
||||
if edge == Edge::Top {
|
||||
"M3 5h18v14H3z M3 9h18"
|
||||
} else {
|
||||
"M3 5h18v14H3z M3 15h18"
|
||||
},
|
||||
palette.accent,
|
||||
))
|
||||
.push(
|
||||
widget::column([])
|
||||
.spacing(3)
|
||||
.push(self.panel_heading(
|
||||
if edge == Edge::Top {
|
||||
"Top panel"
|
||||
} else {
|
||||
"Bottom panel"
|
||||
},
|
||||
14.,
|
||||
))
|
||||
.push(self.note(ferese_config::tr!("Shown on all displays")))
|
||||
.width(cosmic::iced::Length::Fill),
|
||||
)
|
||||
.push(self.settings_button(
|
||||
ferese_config::tr!("Edit panel"),
|
||||
"M4 20l4-1 12-12-3-3L5 16z",
|
||||
Some(Message::PanelPage(PanelPage::Arrange)),
|
||||
false,
|
||||
))
|
||||
.push(
|
||||
self.settings_icon_button(
|
||||
ferese_config::tr!("Remove custom panel"),
|
||||
"M4 7h16 M9 7V4h6v3 M6 7l1 13h10l1-13 M10 11v5 M14 11v5",
|
||||
self.draft.item("panels").is_some().then(|| {
|
||||
Message::Change(store::Edit::Unset("panels".into()))
|
||||
}),
|
||||
)),
|
||||
)
|
||||
.padding(14)
|
||||
.width(cosmic::iced::Length::Fill)
|
||||
.class(crate::visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
),
|
||||
),
|
||||
)
|
||||
.padding(14)
|
||||
.width(cosmic::iced::Length::Fill)
|
||||
.class(crate::visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
let preset = self.preview_panel().ok().and_then(|panel| panel.preset());
|
||||
let position = widget::row([Edge::Top, Edge::Bottom].into_iter().map(|position| {
|
||||
let label = if position == Edge::Top {
|
||||
@@ -152,7 +154,7 @@ impl App {
|
||||
);
|
||||
let mut appearance = widget::column([])
|
||||
.spacing(16)
|
||||
.push(self.panel_heading("Appearance", 15.))
|
||||
.push(self.panel_heading(ferese_config::tr!("Appearance"), 15.))
|
||||
.push(self.panel_style_choices(preset.unwrap_or_default()))
|
||||
.push(self.panel_divider())
|
||||
.push(self.panel_opacity_controls())
|
||||
@@ -166,7 +168,7 @@ impl App {
|
||||
self.draft.boolean("panels.0.border", false),
|
||||
palette,
|
||||
)
|
||||
.name("Outline")
|
||||
.name(ferese_config::tr!("Outline"))
|
||||
.on_press(Message::PanelEdit(Action::SetBorder(
|
||||
!self.draft.boolean("panels.0.border", false),
|
||||
))),
|
||||
@@ -192,7 +194,7 @@ impl App {
|
||||
"Join the island to the screen edge with curved shoulders.",
|
||||
widget::container(
|
||||
ferese_theme::controls::switch(attached, palette)
|
||||
.name("Attach to screen edge")
|
||||
.name(ferese_config::tr!("Attach to screen edge"))
|
||||
.on_press(Message::PanelEdit(Action::SetMorphAttached(
|
||||
!attached,
|
||||
))),
|
||||
@@ -208,7 +210,7 @@ impl App {
|
||||
"Expand the island when you hover over it.",
|
||||
widget::container(
|
||||
ferese_theme::controls::switch(enabled, palette)
|
||||
.name("Preview on hover")
|
||||
.name(ferese_config::tr!("Preview on hover"))
|
||||
.on_press(Message::PanelEdit(Action::SetPeekOnHover(
|
||||
!enabled,
|
||||
))),
|
||||
@@ -248,7 +250,7 @@ impl App {
|
||||
"Show +n for additional activities.",
|
||||
widget::container(
|
||||
ferese_theme::controls::switch(show_badge, palette)
|
||||
.name("Show activity count")
|
||||
.name(ferese_config::tr!("Show activity count"))
|
||||
.on_press(Message::Change(store::set(
|
||||
"activities.show_badge",
|
||||
!show_badge,
|
||||
@@ -272,10 +274,10 @@ impl App {
|
||||
let mut settings = widget::column([]).spacing(4).push(
|
||||
widget::container(
|
||||
widget::row![
|
||||
self.panel_heading("Size & spacing", 15.)
|
||||
self.panel_heading(ferese_config::tr!("Size & spacing"), 15.)
|
||||
.width(cosmic::iced::Length::Fill),
|
||||
self.settings_button(
|
||||
"Reset to style",
|
||||
ferese_config::tr!("Reset to style"),
|
||||
"M3 12a9 9 0 1 0 3-6 M3 3v6h6",
|
||||
pins.then_some(Message::PanelEdit(Action::ResetShape)),
|
||||
false
|
||||
@@ -355,7 +357,7 @@ impl App {
|
||||
);
|
||||
let mut shell = widget::column([])
|
||||
.spacing(4)
|
||||
.push(self.panel_heading("Shell appearance", 15.));
|
||||
.push(self.panel_heading(ferese_config::tr!("Shell appearance"), 15.));
|
||||
for field in crate::schema::fields(Page::Bar)
|
||||
.into_iter()
|
||||
.filter(|field| field.path.starts_with("theme."))
|
||||
@@ -427,7 +429,7 @@ impl App {
|
||||
"Background opacity",
|
||||
"Reduce transparency is on. Turn it off in Accessibility to change opacity.",
|
||||
self.settings_button(
|
||||
"Accessibility",
|
||||
ferese_config::tr!("Accessibility"),
|
||||
"M9 18l6-6-6-6",
|
||||
Some(Message::Page(Page::Accessibility)),
|
||||
false,
|
||||
@@ -482,7 +484,7 @@ impl App {
|
||||
.width(48),
|
||||
)
|
||||
.push(self.settings_icon_button(
|
||||
"Use shell opacity",
|
||||
ferese_config::tr!("Use shell opacity"),
|
||||
"M3 12a9 9 0 1 0 3-6 M3 3v6h6",
|
||||
custom.then_some(Message::PanelEdit(Action::ClearOpacity)),
|
||||
));
|
||||
@@ -632,8 +634,10 @@ impl App {
|
||||
cosmic::iced::widget::responsive(move |size| {
|
||||
let editor = widget::column([])
|
||||
.spacing(12)
|
||||
.push(self.panel_heading("Arrangement", 15.))
|
||||
.push(self.note("Drag items to reorder. Select an item or group to edit."))
|
||||
.push(self.panel_heading(ferese_config::tr!("Arrangement"), 15.))
|
||||
.push(self.note(ferese_config::tr!(
|
||||
"Drag items to reorder. Select an item or group to edit."
|
||||
)))
|
||||
.push(
|
||||
widget::scrollable(self.panel_editor(&panel))
|
||||
.id(widget::Id::new("settings-content"))
|
||||
@@ -710,9 +714,9 @@ impl App {
|
||||
"M4 4h16v16H4z M14 4v16 M7 8h4 M7 12h4",
|
||||
palette.muted,
|
||||
))
|
||||
.push(self.panel_heading("Make it your own", 15.))
|
||||
.push(self.panel_heading(ferese_config::tr!("Make it your own"), 15.))
|
||||
.push(self.note(
|
||||
"Select an item to adjust how it appears, or a group to edit its placement and spacing.",
|
||||
ferese_config::tr!("Select an item to adjust how it appears, or a group to edit its placement and spacing."),
|
||||
)),
|
||||
)
|
||||
.padding([40, 8])
|
||||
@@ -779,7 +783,7 @@ impl App {
|
||||
)
|
||||
.push(
|
||||
self.settings_button(
|
||||
"Add group",
|
||||
ferese_config::tr!("Add group"),
|
||||
"M12 5v14 M5 12h14",
|
||||
(panel.group_count() < ferese_panel::MAX_GROUPS)
|
||||
.then_some(Message::PanelEdit(Action::AddGroup(placement))),
|
||||
@@ -881,8 +885,10 @@ impl App {
|
||||
}
|
||||
let mut contents = widget::column([]).spacing(4).push(heading);
|
||||
if items.is_empty() {
|
||||
contents = contents
|
||||
.push(widget::container(self.note("Drop items here")).padding([8, 6]));
|
||||
contents = contents.push(
|
||||
widget::container(self.note(ferese_config::tr!("Drop items here")))
|
||||
.padding([8, 6]),
|
||||
);
|
||||
} else {
|
||||
contents = contents.push(
|
||||
widget::flex_row(items)
|
||||
@@ -922,12 +928,14 @@ impl App {
|
||||
}
|
||||
if zone.groups.is_empty() {
|
||||
groups = groups.push(
|
||||
widget::container(self.note("Drop an item here, or add a group."))
|
||||
.id(crate::panel_drag::empty_zone_id(placement))
|
||||
.width(Length::Fill)
|
||||
.height(64)
|
||||
.padding(12)
|
||||
.class(crate::visuals::surface(palette.card, 10.)),
|
||||
widget::container(
|
||||
self.note(ferese_config::tr!("Drop an item here, or add a group.")),
|
||||
)
|
||||
.id(crate::panel_drag::empty_zone_id(placement))
|
||||
.width(Length::Fill)
|
||||
.height(64)
|
||||
.padding(12)
|
||||
.class(crate::visuals::surface(palette.card, 10.)),
|
||||
);
|
||||
}
|
||||
zones = zones.push(
|
||||
@@ -951,6 +959,7 @@ pub(super) fn group_name(id: &str) -> String {
|
||||
pub(super) fn item_icon(kind: ItemKind, tint: cosmic::iced::Color) -> widget::icon::Icon {
|
||||
use ferese_theme::icons;
|
||||
let source = match kind {
|
||||
ItemKind::Tray => icons::APPLICATION,
|
||||
ItemKind::Overview => icons::FERESE,
|
||||
ItemKind::Workspaces { .. } => icons::OVERVIEW,
|
||||
ItemKind::FocusedWindow => icons::DISPLAY,
|
||||
|
||||
@@ -23,7 +23,7 @@ impl App {
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(self.settings_icon_button(
|
||||
"Deselect",
|
||||
ferese_config::tr!("Deselect"),
|
||||
"M6 6l12 12 M6 18L18 6",
|
||||
Some(Message::PanelClearSelection),
|
||||
)),
|
||||
@@ -58,7 +58,7 @@ impl App {
|
||||
rows = rows.push(
|
||||
widget::column([])
|
||||
.spacing(8)
|
||||
.push(self.label("Placement", 13.))
|
||||
.push(self.label(ferese_config::tr!("Placement"), 13.))
|
||||
.push(crate::visuals::panel_select(
|
||||
cosmic::iced::widget::pick_list(
|
||||
crate::panel_edit::destinations(panel),
|
||||
@@ -166,8 +166,10 @@ impl App {
|
||||
};
|
||||
let mut choices = widget::column([])
|
||||
.spacing(6)
|
||||
.push(self.label("Workspace style", 13.))
|
||||
.push(self.note("A sample of active, occupied and empty workspaces."));
|
||||
.push(self.label(ferese_config::tr!("Workspace style"), 13.))
|
||||
.push(self.note(ferese_config::tr!(
|
||||
"A sample of active, occupied and empty workspaces."
|
||||
)));
|
||||
for style in ferese_panel::WorkspaceStyle::ALL {
|
||||
let sample = ferese_theme::workspaces::sample(style, palette, self.font, 22.);
|
||||
choices = choices.push(
|
||||
@@ -210,7 +212,7 @@ impl App {
|
||||
.push(
|
||||
widget::column([])
|
||||
.spacing(8)
|
||||
.push(self.label("Placement", 13.))
|
||||
.push(self.label(ferese_config::tr!("Placement"), 13.))
|
||||
.push(crate::visuals::panel_select(
|
||||
cosmic::iced::widget::pick_list(
|
||||
Zone::ALL.to_vec(),
|
||||
@@ -307,8 +309,9 @@ impl App {
|
||||
),
|
||||
);
|
||||
if !group.items.is_empty() {
|
||||
rows = rows
|
||||
.push(self.note("Move or remove this group’s items before removing the group."));
|
||||
rows = rows.push(self.note(ferese_config::tr!(
|
||||
"Move or remove this group’s items before removing the group."
|
||||
)));
|
||||
}
|
||||
rows.into()
|
||||
}
|
||||
@@ -322,8 +325,8 @@ impl App {
|
||||
) -> Element<'static, Message> {
|
||||
widget::row([])
|
||||
.spacing(8)
|
||||
.push(self.settings_icon_button("Up", "M6 15l6-6 6 6", earlier))
|
||||
.push(self.settings_icon_button("Down", "M6 9l6 6 6-6", later))
|
||||
.push(self.settings_icon_button(ferese_config::tr!("Up"), "M6 15l6-6 6 6", earlier))
|
||||
.push(self.settings_icon_button(ferese_config::tr!("Down"), "M6 9l6 6 6-6", later))
|
||||
.push(self.settings_icon_button(
|
||||
remove_label,
|
||||
"M4 6h16 M9 6V3h6v3 M6 6l1 15h10l1-15 M10 10v7 M14 10v7",
|
||||
|
||||
@@ -7,6 +7,7 @@ use ferese_theme::panel::{Sample, Spacing, frame};
|
||||
use crate::{App, Element, Message, panel_controls, visuals};
|
||||
|
||||
const SAMPLE_AVAILABILITY: Availability = Availability {
|
||||
tray: true,
|
||||
capture: true,
|
||||
activities: true,
|
||||
media: true,
|
||||
@@ -47,8 +48,11 @@ impl App {
|
||||
.push(
|
||||
row([])
|
||||
.align_y(Alignment::Center)
|
||||
.push(self.label("Composition preview", 14.).width(Length::Fill))
|
||||
.push(self.note("Sample content")),
|
||||
.push(
|
||||
self.label(ferese_config::tr!("Composition preview"), 14.)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(self.note(ferese_config::tr!("Sample content"))),
|
||||
)
|
||||
.push(
|
||||
container(self.panel_preview_frame(panel.clone()))
|
||||
@@ -121,7 +125,7 @@ impl App {
|
||||
if panel.style == ferese_panel::PanelStyle::Morph {
|
||||
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||||
let height = panel.resolved_geometry().height;
|
||||
let hub = container(self.label("● 10:09", 13.))
|
||||
let hub = container(self.label(ferese_config::tr!("● 10:09"), 13.))
|
||||
.id("morph-preview-body")
|
||||
.center_x(panel.morph.idle_width as f32)
|
||||
.center_y(height);
|
||||
@@ -383,7 +387,7 @@ impl App {
|
||||
let sample = ferese_theme::workspaces::sample(style, palette, self.font, 20.);
|
||||
let control =
|
||||
button::custom(container(sample).id(format!("preview-icon:{}", item.id.0)))
|
||||
.name("Edit Workspaces")
|
||||
.name(ferese_config::tr!("Edit Workspaces"))
|
||||
.padding([0, 2])
|
||||
.height(control_height)
|
||||
.on_press(Message::PanelSelect(item.id.clone()))
|
||||
@@ -408,6 +412,7 @@ impl App {
|
||||
ItemKind::Network => icons::WIFI_FULL,
|
||||
ItemKind::Audio => icons::VOLUME_HIGH,
|
||||
ItemKind::Recording => icons::RECORD,
|
||||
ItemKind::Tray => icons::APPLICATION,
|
||||
ItemKind::CaptureStatus => icons::DISPLAY,
|
||||
ItemKind::Notifications => icons::NOTIFICATIONS,
|
||||
ItemKind::Battery { .. } => icons::BATTERY_75,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Page {
|
||||
Appearance,
|
||||
General,
|
||||
Theme,
|
||||
Tray,
|
||||
Configuration,
|
||||
Accessibility,
|
||||
Wallpaper,
|
||||
Bar,
|
||||
@@ -16,8 +20,12 @@ pub enum Page {
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub const NAVIGATION: [(&'static str, &'static [Self]); 3] = [
|
||||
("Desktop", &[Self::Appearance, Self::Wallpaper, Self::Bar]),
|
||||
pub const NAVIGATION: [(&'static str, &'static [Self]); 5] = [
|
||||
("Appearance", &[Self::Appearance, Self::General]),
|
||||
(
|
||||
"Desktop",
|
||||
&[Self::Theme, Self::Wallpaper, Self::Bar, Self::Tray],
|
||||
),
|
||||
(
|
||||
"Interaction",
|
||||
&[Self::Windows, Self::Keyboard, Self::Accessibility],
|
||||
@@ -32,6 +40,7 @@ impl Page {
|
||||
Self::Startup,
|
||||
],
|
||||
),
|
||||
("Advanced", &[Self::Configuration]),
|
||||
];
|
||||
|
||||
/// Subpages share one entry in navigation and search.
|
||||
@@ -66,8 +75,12 @@ impl Page {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub const ALL: [Self; 13] = [
|
||||
pub const ALL: [Self; 17] = [
|
||||
Self::Appearance,
|
||||
Self::General,
|
||||
Self::Theme,
|
||||
Self::Tray,
|
||||
Self::Configuration,
|
||||
Self::Accessibility,
|
||||
Self::Wallpaper,
|
||||
Self::Bar,
|
||||
@@ -83,8 +96,15 @@ impl Page {
|
||||
];
|
||||
|
||||
pub fn title(self) -> &'static str {
|
||||
ferese_config::locale::text(self.title_key())
|
||||
}
|
||||
fn title_key(self) -> &'static str {
|
||||
match self {
|
||||
Self::Appearance => "Appearance",
|
||||
Self::General => "General",
|
||||
Self::Tray => "Tray icons",
|
||||
Self::Configuration => "All options",
|
||||
Self::Theme => "Theme",
|
||||
Self::Accessibility => "Accessibility",
|
||||
Self::Wallpaper => "Wallpaper",
|
||||
Self::Bar => "Panel & Shell",
|
||||
@@ -101,8 +121,14 @@ impl Page {
|
||||
}
|
||||
|
||||
pub fn subtitle(self) -> &'static str {
|
||||
match self {
|
||||
Self::Appearance => "A desktop that feels like yours.",
|
||||
ferese_config::locale::text(match self {
|
||||
Self::Appearance => "All Ferese settings, grouped by function.",
|
||||
Self::General => "Language and desktop preferences.",
|
||||
Self::Tray => "Choose which status and application icons appear in the panel.",
|
||||
Self::Configuration => {
|
||||
"Every supported configuration option, including advanced settings."
|
||||
}
|
||||
Self::Theme => "A desktop that feels like yours.",
|
||||
Self::Accessibility => "Make the desktop easier to see and use.",
|
||||
Self::Wallpaper => "Set the scene for your workspace.",
|
||||
Self::Bar => "Arrange your panel and change the shell's appearance.",
|
||||
@@ -115,7 +141,7 @@ impl Page {
|
||||
Self::Startup => "Ready when you sign in.",
|
||||
Self::Displays => "A place for every screen.",
|
||||
Self::Connections => "Wi-Fi networks and Bluetooth devices.",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn icon(self) -> &'static str {
|
||||
@@ -123,7 +149,11 @@ impl Page {
|
||||
Self::Accessibility => {
|
||||
"M12 3a2 2 0 1 0 0 4a2 2 0 1 0 0-4 M4 9h16 M12 9v6 M12 15l-5 6 M12 15l5 6"
|
||||
}
|
||||
Self::Appearance => {
|
||||
Self::Appearance | Self::General | Self::Configuration => {
|
||||
"M4 5h16 M4 12h16 M4 19h16 M8 3v4 M16 10v4 M10 17v4"
|
||||
}
|
||||
Self::Tray => "M3 5h18v14H3z M6 9h3v3H6z M12 9h3v3h-3z M18 9h.01",
|
||||
Self::Theme => {
|
||||
"M12 3a9 9 0 1 0 0 18h1a2 2 0 0 0 0-4h-1a1 1 0 0 1 0-2h3a6 6 0 0 0 0-12z M7 9h.01 M10 6h.01 M15 6h.01 M18 10h.01"
|
||||
}
|
||||
Self::Wallpaper => "M4 4h16v16H4z M4 16l5-5 4 4 3-3 4 4 M15 8h.01",
|
||||
@@ -157,11 +187,31 @@ impl Page {
|
||||
fn matches_own(self, query: &str) -> bool {
|
||||
let query = query.to_lowercase();
|
||||
self.title().to_lowercase().contains(&query)
|
||||
|| self.title_key().to_lowercase().contains(&query)
|
||||
|| ferese_config::locale::in_language(
|
||||
ferese_config::locale::Language::Chinese,
|
||||
self.title_key(),
|
||||
)
|
||||
.to_lowercase()
|
||||
.contains(&query)
|
||||
|| self.subtitle().to_lowercase().contains(&query)
|
||||
|| fields(self).iter().any(|f| {
|
||||
format!("{} {} {}", f.label, f.description, f.path)
|
||||
.to_lowercase()
|
||||
.contains(&query)
|
||||
format!(
|
||||
"{} {} {} {} {}",
|
||||
f.label,
|
||||
f.description,
|
||||
f.path,
|
||||
ferese_config::locale::in_language(
|
||||
ferese_config::locale::Language::Chinese,
|
||||
&f.label
|
||||
),
|
||||
ferese_config::locale::in_language(
|
||||
ferese_config::locale::Language::Chinese,
|
||||
&f.description
|
||||
)
|
||||
)
|
||||
.to_lowercase()
|
||||
.contains(&query)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -375,7 +425,7 @@ pub fn fields(page: Page) -> Vec<Field> {
|
||||
false,
|
||||
),
|
||||
],
|
||||
Page::Appearance => vec![
|
||||
Page::Theme => vec![
|
||||
choice(
|
||||
"theme.mode",
|
||||
"Appearance",
|
||||
@@ -910,6 +960,57 @@ pub fn fields(page: Page) -> Vec<Field> {
|
||||
"Keep the shortcut guide until you are comfortable with Ferese.",
|
||||
true,
|
||||
)],
|
||||
Page::Startup | Page::Displays | Page::Connections => vec![],
|
||||
Page::General => vec![choice(
|
||||
"ui.language",
|
||||
"Language",
|
||||
"Changes the Ferese interface language. Applies immediately.",
|
||||
"auto",
|
||||
&[
|
||||
("auto", "Follow system"),
|
||||
("zh-CN", "简体中文"),
|
||||
("en", "English"),
|
||||
],
|
||||
)],
|
||||
Page::Tray => vec![
|
||||
toggle(
|
||||
"status.tray.enabled",
|
||||
"Show tray icons",
|
||||
"Show application icons in panel tray items.",
|
||||
true,
|
||||
),
|
||||
toggle(
|
||||
"status.tray.show_passive",
|
||||
"Show passive icons",
|
||||
"Include applications without active status.",
|
||||
false,
|
||||
),
|
||||
range(
|
||||
"status.tray.icon_size",
|
||||
"Tray icon size",
|
||||
"Size of application tray icons.",
|
||||
RangeSpec {
|
||||
default: 16.,
|
||||
min: 8.,
|
||||
max: 48.,
|
||||
step: 1.,
|
||||
suffix: " px",
|
||||
integer: true,
|
||||
},
|
||||
),
|
||||
Field::new(
|
||||
"status.tray.hidden_items",
|
||||
"Hidden applications",
|
||||
"Application IDs to hide, separated by spaces.",
|
||||
Kind::Text {
|
||||
default: "",
|
||||
argv: true,
|
||||
},
|
||||
),
|
||||
],
|
||||
Page::Appearance
|
||||
| Page::Configuration
|
||||
| Page::Startup
|
||||
| Page::Displays
|
||||
| Page::Connections => vec![],
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ impl App {
|
||||
let labels = column([])
|
||||
.spacing(2)
|
||||
.width(Length::Fill)
|
||||
.push(self.label("Theme overrides", 13.))
|
||||
.push(self.label(ferese_config::tr!("Theme overrides"), 13.))
|
||||
.push(widget::tooltip(
|
||||
self.label(filename, 11.)
|
||||
.class(cosmic::theme::Text::Color(palette.muted)),
|
||||
@@ -106,7 +106,7 @@ impl App {
|
||||
button::custom(visuals::action_icon("M6 6l12 12M6 18L18 6", palette.muted))
|
||||
.padding(4)
|
||||
.on_press(Message::Change(Edit::Unset(path.into()))),
|
||||
self.label("Remove override", 11.),
|
||||
self.label(ferese_config::tr!("Remove override"), 11.),
|
||||
widget::tooltip::Position::Bottom,
|
||||
));
|
||||
}
|
||||
@@ -152,7 +152,7 @@ impl App {
|
||||
column([])
|
||||
.spacing(2)
|
||||
.width(Length::Fill)
|
||||
.push(self.label("Automatic appearance", 13.))
|
||||
.push(self.label(ferese_config::tr!("Automatic appearance"), 13.))
|
||||
.push(self.note(description)),
|
||||
)
|
||||
.push(sources),
|
||||
@@ -221,18 +221,18 @@ impl App {
|
||||
);
|
||||
if appearance != Some(Dark) && self.undo.is_some() {
|
||||
heading = heading.push(self.settings_icon_button(
|
||||
"Undo",
|
||||
ferese_config::tr!("Undo"),
|
||||
"M4 10h8 M4 10V3 M4 10c3-7 16-6 16 3a7 7 0 0 1-7 7",
|
||||
(!self.saving).then_some(Message::Undo),
|
||||
));
|
||||
}
|
||||
let mut custom_heading = row([])
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(6)
|
||||
.push(self.label("Custom themes", 14.).width(Length::Fill));
|
||||
let mut custom_heading = row([]).align_y(Alignment::Center).spacing(6).push(
|
||||
self.label(ferese_config::tr!("Custom themes"), 14.)
|
||||
.width(Length::Fill),
|
||||
);
|
||||
if appearance != Some(Dark) {
|
||||
custom_heading = custom_heading.push(self.settings_button(
|
||||
"Import…",
|
||||
ferese_config::tr!("Import…"),
|
||||
"M3 7V5h6l2 2h10v13H3z M12 10v7 M9 14l3 3 3-3",
|
||||
Some(Message::ImportTheme),
|
||||
false,
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
//! Read tray applications through the existing watcher without becoming a host.
|
||||
use super::{Alignment, App, Element, Length, Message, Task, column, row, set};
|
||||
use ferese_config::locale::text as t;
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Application {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
}
|
||||
async fn applications() -> Result<Vec<Application>, String> {
|
||||
let connection = zbus::Connection::session()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let watcher = zbus::Proxy::new(
|
||||
&connection,
|
||||
"org.kde.StatusNotifierWatcher",
|
||||
"/StatusNotifierWatcher",
|
||||
"org.kde.StatusNotifierWatcher",
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let addresses: Vec<String> = watcher
|
||||
.get_property("RegisteredStatusNotifierItems")
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut apps = Vec::new();
|
||||
for address in addresses.into_iter().take(128) {
|
||||
let (name, path) = address
|
||||
.find('/')
|
||||
.map_or((address.as_str(), "/StatusNotifierItem"), |i| {
|
||||
(&address[..i], &address[i..])
|
||||
});
|
||||
let Ok(proxy) =
|
||||
zbus::Proxy::new(&connection, name, path, "org.kde.StatusNotifierItem").await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Ok(id) = proxy.get_property::<String>("Id").await else {
|
||||
continue;
|
||||
};
|
||||
let title = proxy
|
||||
.get_property::<String>("Title")
|
||||
.await
|
||||
.unwrap_or_else(|_| id.clone());
|
||||
if !apps.iter().any(|a: &Application| a.id == id) {
|
||||
apps.push(Application { id, title });
|
||||
}
|
||||
}
|
||||
apps.sort_by(|a, b| a.title.cmp(&b.title));
|
||||
Ok(apps)
|
||||
}
|
||||
impl App {
|
||||
pub(super) fn load_tray_apps(&self) -> Task<Message> {
|
||||
cosmic::task::future(async { Message::TrayApps(applications().await.unwrap_or_default()) })
|
||||
}
|
||||
pub(super) fn tray_application_controls(&self) -> Element<'_, Message> {
|
||||
let hidden = self
|
||||
.draft
|
||||
.item("status.tray.hidden_items")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.filter_map(|v| v.as_str().map(str::to_owned))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut apps = self.tray_apps.clone();
|
||||
for id in &hidden {
|
||||
if !apps.iter().any(|a| &a.id == id) {
|
||||
apps.push(Application {
|
||||
id: id.clone(),
|
||||
title: id.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut out = column([])
|
||||
.spacing(8)
|
||||
.push(self.panel_heading(t("Application icons"), 15.));
|
||||
for app in apps {
|
||||
let visible = !hidden.contains(&app.id);
|
||||
let mut next = hidden.clone();
|
||||
if visible {
|
||||
next.push(app.id);
|
||||
} else {
|
||||
next.retain(|id| id != &app.id);
|
||||
}
|
||||
out = out.push(
|
||||
row([])
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center)
|
||||
.push(self.label(app.title, 13.).width(Length::Fill))
|
||||
.push(
|
||||
ferese_theme::controls::switch(
|
||||
visible,
|
||||
super::visuals::Palette::from_resolved(&self.resolved.presented),
|
||||
)
|
||||
.on_press(Message::Change(set(
|
||||
"status.tray.hidden_items",
|
||||
serde_json::json!(next),
|
||||
))),
|
||||
),
|
||||
);
|
||||
}
|
||||
out.push(self.settings_button(
|
||||
t("Refresh applications"),
|
||||
"M3 10a8 8 0 1 1 1 7 M3 3v7h7",
|
||||
Some(Message::RefreshTrayApps),
|
||||
false,
|
||||
))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
@@ -343,7 +343,7 @@ impl App {
|
||||
let preview = &self.wallpaper.active;
|
||||
let mut body = column([])
|
||||
.spacing(8)
|
||||
.push(self.label("Current wallpaper", 13.));
|
||||
.push(self.label(ferese_config::tr!("Current wallpaper"), 13.));
|
||||
let current: Element<'_, Message> = if let Some(handle) = &preview.handle {
|
||||
widget::image(handle.clone())
|
||||
.border_radius(palette.radius.min(10.))
|
||||
@@ -376,7 +376,7 @@ impl App {
|
||||
row([])
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center)
|
||||
.push(self.label("Apply to", 12.))
|
||||
.push(self.label(ferese_config::tr!("Apply to"), 12.))
|
||||
.push(ferese_theme::controls::select(
|
||||
cosmic::iced::widget::pick_list(
|
||||
Target::ALL,
|
||||
@@ -389,7 +389,7 @@ impl App {
|
||||
))
|
||||
.push(widget::Space::new().width(Length::Fill))
|
||||
.push(self.settings_button(
|
||||
"Choose image…",
|
||||
ferese_config::tr!("Choose image…"),
|
||||
"M3 4h18v16H3z M3 15l5-5 5 5 3-3 5 5 M15 8h.01",
|
||||
Some(Message::PickWallpaper),
|
||||
false,
|
||||
@@ -444,10 +444,10 @@ impl App {
|
||||
row([])
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center)
|
||||
.push(self.label("Default wallpapers", 13.))
|
||||
.push(self.label(ferese_config::tr!("Default wallpapers"), 13.))
|
||||
.push(widget::Space::new().width(Length::Fill))
|
||||
.push(self.settings_button(
|
||||
"Match appearance",
|
||||
ferese_config::tr!("Match appearance"),
|
||||
"M20 7v5h-5 M4 17v-5h5 M6 7a7 7 0 0 1 12-2l2 2 M18 17a7 7 0 0 1-12 2l-2-2",
|
||||
Some(Message::RestoreDefaultWallpapers),
|
||||
false,
|
||||
|
||||
@@ -32,6 +32,7 @@ tempfile.workspace = true
|
||||
tokio = { version = "1", features = ["time", "rt", "sync", "net", "macros"] }
|
||||
wayland-client.workspace = true
|
||||
zbus = "5.19.0"
|
||||
system-tray = { version = "0.8.9", default-features = false, features = ["data"] }
|
||||
pipewire = { version = "0.10.1", features = ["v0_3_65"] }
|
||||
image-codec = { package = "image", version = "0.25.10", default-features = false, features = ["png", "jpeg", "webp"] }
|
||||
url = "2.5.8"
|
||||
|
||||
@@ -583,7 +583,12 @@ impl FereseShell {
|
||||
) -> (Element<'_, cosmic::Action<Message>>, bool) {
|
||||
use ferese_panel::Representation;
|
||||
let Some(presentation) = self.activities.flat.get(&id) else {
|
||||
return (container(self.render.text("")).width(0).into(), false);
|
||||
return (
|
||||
container(self.render.text(ferese_config::tr!("")))
|
||||
.width(0)
|
||||
.into(),
|
||||
false,
|
||||
);
|
||||
};
|
||||
let Some(activity) = presentation.selection.visible.first().and_then(|id| {
|
||||
self.activities
|
||||
@@ -593,7 +598,12 @@ impl FereseShell {
|
||||
.chain(self.activities.native.iter())
|
||||
.find(|entry| &entry.id == id)
|
||||
}) else {
|
||||
return (container(self.render.text("")).width(0).into(), false);
|
||||
return (
|
||||
container(self.render.text(ferese_config::tr!("")))
|
||||
.width(0)
|
||||
.into(),
|
||||
false,
|
||||
);
|
||||
};
|
||||
let palette = self.config.theme.palette();
|
||||
let result_age = activity.result_deadline.map(|deadline| {
|
||||
@@ -1089,7 +1099,7 @@ impl FereseShell {
|
||||
rows.push(
|
||||
container(
|
||||
self.render
|
||||
.text("No active tasks")
|
||||
.text(ferese_config::tr!("No active tasks"))
|
||||
.size(12)
|
||||
.class(theme::Text::Color(palette.muted.scale_alpha(opacity))),
|
||||
)
|
||||
@@ -1167,7 +1177,7 @@ impl FereseShell {
|
||||
{
|
||||
body = body.push(
|
||||
button::custom(icons::tinted(icons::MEDIA_PLAY, 16, palette.text))
|
||||
.name("Play or pause")
|
||||
.name(ferese_config::tr!("Play or pause"))
|
||||
.on_press(cosmic::Action::App(Message::MediaAction(
|
||||
self.media.action("play-pause"),
|
||||
))),
|
||||
|
||||
@@ -122,7 +122,7 @@ impl FereseShell {
|
||||
badge,
|
||||
column![
|
||||
self.render
|
||||
.text("Pomodoro")
|
||||
.text(ferese_config::tr!("Pomodoro"))
|
||||
.size(13)
|
||||
.class(theme::Text::Color(primary)),
|
||||
self.render
|
||||
|
||||
@@ -75,7 +75,7 @@ impl FereseShell {
|
||||
|
||||
pub(super) fn view_layer(&self, id: window::Id) -> Element<'_, cosmic::Action<Message>> {
|
||||
if self.bar_hidden(id) {
|
||||
return container(self.render.text(""))
|
||||
return container(self.render.text(ferese_config::tr!("")))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into();
|
||||
@@ -94,6 +94,7 @@ impl FereseShell {
|
||||
let effects = output.and_then(|output| output.effects.as_ref());
|
||||
let compositor_material = effects.is_some();
|
||||
let availability = Availability {
|
||||
tray: self.tray.available(&self.config.status.tray),
|
||||
capture: self.capture_status.active(),
|
||||
activities: self.activity_available(id),
|
||||
media: self.media.snapshot.selected.is_some(),
|
||||
@@ -324,9 +325,12 @@ impl FereseShell {
|
||||
cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(1),
|
||||
));
|
||||
let badge = container(
|
||||
row![title, self.render.text("Esc to exit").size(12)]
|
||||
.spacing(12)
|
||||
.align_y(cosmic::iced::Alignment::Center),
|
||||
row![
|
||||
title,
|
||||
self.render.text(ferese_config::tr!("Esc to exit")).size(12)
|
||||
]
|
||||
.spacing(12)
|
||||
.align_y(cosmic::iced::Alignment::Center),
|
||||
)
|
||||
.id(badge_id.clone())
|
||||
.padding([0, 12])
|
||||
@@ -480,6 +484,9 @@ impl FereseShell {
|
||||
control_height,
|
||||
);
|
||||
}
|
||||
ItemKind::Tray => {
|
||||
return (self.view_tray(id), false);
|
||||
}
|
||||
ItemKind::Media => self.view_media_item(representation, selected, width),
|
||||
ItemKind::QuickSettings => {
|
||||
self.view_status_item(status_ui::Menu::System, false, selected)
|
||||
@@ -535,7 +542,7 @@ impl FereseShell {
|
||||
button::custom(bar_content(content, bar.group_item_height))
|
||||
.padding([0, 4])
|
||||
.height(bar.group_item_height)
|
||||
.name("Open calendar")
|
||||
.name(ferese_config::tr!("Open calendar"))
|
||||
.on_press_with_rectangle(move |offset, bounds| {
|
||||
cosmic::Action::App(Message::OpenMenu(
|
||||
status_ui::Menu::Calendar,
|
||||
@@ -558,7 +565,7 @@ impl FereseShell {
|
||||
bar_icon(ferese_theme::icons::DISPLAY, bar.icon_size, foreground),
|
||||
control_height,
|
||||
))
|
||||
.name("Display mode")
|
||||
.name(ferese_config::tr!("Display mode"))
|
||||
.height(control_height)
|
||||
.padding([
|
||||
0.0,
|
||||
|
||||
@@ -8,6 +8,7 @@ const DEFAULT_BACKGROUND: [u8; 3] = [11, 15, 20];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ShellConfig {
|
||||
pub(crate) ui: ferese_config::locale::Settings,
|
||||
pub(crate) terminal: Vec<String>,
|
||||
pub(crate) activities: ferese_config::activities::Settings,
|
||||
pub(crate) notifications: ferese_config::notifications::NotificationConfig,
|
||||
@@ -24,6 +25,7 @@ impl Default for ShellConfig {
|
||||
fn default() -> Self {
|
||||
let status = StatusConfig::default();
|
||||
Self {
|
||||
ui: Default::default(),
|
||||
terminal: vec!["foot".into()],
|
||||
activities: Default::default(),
|
||||
notifications: Default::default(),
|
||||
@@ -38,23 +40,7 @@ impl Default for ShellConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub(crate) struct StatusConfig {
|
||||
pub(crate) keybinding_guide: bool,
|
||||
pub(crate) low_battery_threshold: u8,
|
||||
pub(crate) settings_command: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for StatusConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
keybinding_guide: true,
|
||||
low_battery_threshold: 20,
|
||||
settings_command: Some(vec!["ferese-settings".into()]),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) use ferese_config::status::Settings as StatusConfig;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct ShellTheme {
|
||||
@@ -190,6 +176,8 @@ pub(crate) enum WallpaperMode {
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct FereseConfig {
|
||||
#[serde(default)]
|
||||
ui: ferese_config::locale::Settings,
|
||||
#[serde(default)]
|
||||
commands: std::collections::HashMap<String, Vec<String>>,
|
||||
#[serde(default)]
|
||||
@@ -405,6 +393,10 @@ fn parse_document(
|
||||
.map_err(|error| ferese_config::Error::from(error.to_string()))
|
||||
{
|
||||
Ok(config) => {
|
||||
config
|
||||
.status
|
||||
.validate()
|
||||
.map_err(ferese_config::Error::from)?;
|
||||
if let Some(panels) = &config.panels {
|
||||
ferese_panel::validate(panels).map_err(ferese_config::Error::from)?;
|
||||
}
|
||||
@@ -424,6 +416,7 @@ fn parse_document(
|
||||
theme.bar_radius = theme.material_radius;
|
||||
|
||||
Ok(ShellConfig {
|
||||
ui: config.ui,
|
||||
terminal: config
|
||||
.commands
|
||||
.get("terminal")
|
||||
|
||||
@@ -415,7 +415,9 @@ impl FereseShell {
|
||||
.is_some_and(|modal| modal.focus_visible);
|
||||
let pending = model.pending();
|
||||
let mut header = row![
|
||||
self.render.text("Display mode").size(17),
|
||||
self.render
|
||||
.text(ferese_config::tr!("Display mode"))
|
||||
.size(17),
|
||||
cosmic::iced::widget::Space::new().width(Length::Fill)
|
||||
]
|
||||
.align_y(cosmic::iced::Alignment::Center)
|
||||
|
||||
@@ -141,7 +141,7 @@ impl FereseShell {
|
||||
suggestion = suggestion.push(
|
||||
container(
|
||||
self.render
|
||||
.text("Tab")
|
||||
.text(ferese_config::tr!("Tab"))
|
||||
.size(12)
|
||||
.line_height(cosmic::iced::widget::text::LineHeight::Absolute(14.into()))
|
||||
.class(theme::Text::Color(muted)),
|
||||
@@ -440,7 +440,7 @@ impl FereseShell {
|
||||
if launcher.rows.is_empty() {
|
||||
cards = cards.push(
|
||||
self.render
|
||||
.text("No open windows")
|
||||
.text(ferese_config::tr!("No open windows"))
|
||||
.size(14)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ mod activities;
|
||||
mod bar;
|
||||
mod battery_indicator;
|
||||
mod surfaces;
|
||||
mod tray;
|
||||
|
||||
use bar::{bar_content, output_bar_hidden};
|
||||
use surfaces::{EffectsBinding, EffectsContext, native_wayland_surface};
|
||||
@@ -151,6 +152,7 @@ type WallpaperLoad =
|
||||
cosmic::iced::futures::channel::oneshot::Receiver<Result<image::Handle, String>>;
|
||||
|
||||
struct FereseShell {
|
||||
tray: tray::Model,
|
||||
core: Core,
|
||||
config: ShellConfig,
|
||||
theme_snapshot: ferese_ipc::theme::Snapshot,
|
||||
@@ -205,6 +207,8 @@ struct OutputSurfaces {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Message {
|
||||
Tray(tray::Event),
|
||||
TrayInput(tray::Input),
|
||||
Launcher(launcher::Message),
|
||||
LauncherEnter(window::Id, bool),
|
||||
NightLightTemperature(u16),
|
||||
@@ -357,6 +361,7 @@ impl cosmic::Application for FereseShell {
|
||||
status_service: status::Service::start(config.status.settings_command.clone()),
|
||||
status: status::Snapshot::default(),
|
||||
recorder: recording::Recorder::default(),
|
||||
tray: tray::Model::default(),
|
||||
capture_status: capture_status::Model::new(),
|
||||
media: media::Model::default(),
|
||||
activities: activities::Model::default(),
|
||||
@@ -453,6 +458,7 @@ impl cosmic::Application for FereseShell {
|
||||
activities::subscription::subscription().map(Message::ActivitiesUpdated),
|
||||
self.activity_tick_subscription()
|
||||
.map(|_| Message::ActivityTick),
|
||||
tray::subscription().map(Message::Tray),
|
||||
self.media.subscription().map(Message::MediaUpdated),
|
||||
self.media_tick_subscription().map(|_| Message::MediaTick),
|
||||
self.media.art.subscription().map(Message::MediaArt),
|
||||
@@ -478,6 +484,11 @@ impl cosmic::Application for FereseShell {
|
||||
|
||||
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
|
||||
let task = match message {
|
||||
Message::Tray(event) => {
|
||||
self.tray.update(event);
|
||||
Task::none()
|
||||
}
|
||||
Message::TrayInput(input) => self.tray_input(input),
|
||||
Message::Launcher(message) => self.update_launcher(message),
|
||||
Message::LauncherEnter(id, alt) => {
|
||||
if self.launcher.as_ref().is_some_and(|l| l.id == id) {
|
||||
@@ -1150,11 +1161,11 @@ impl cosmic::Application for FereseShell {
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<'_, Self::Message> {
|
||||
self.render.text("").into()
|
||||
self.render.text(ferese_config::tr!("")).into()
|
||||
}
|
||||
|
||||
fn view_window(&self, _id: window::Id) -> Element<'_, Self::Message> {
|
||||
self.render.text("").into()
|
||||
self.render.text(ferese_config::tr!("")).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1185,6 +1196,8 @@ impl FereseShell {
|
||||
}
|
||||
|
||||
fn apply_config(&mut self, mut config: config::ShellConfig) -> Task<Message> {
|
||||
ferese_config::locale::set(config.ui.language);
|
||||
self.clock = clock::current_time();
|
||||
if std::env::var_os("FERESE_COMPOSITOR_WALLPAPER").is_some() {
|
||||
config.wallpaper.path = None;
|
||||
}
|
||||
@@ -1386,6 +1399,7 @@ mod tests {
|
||||
status_service: status::Service::start(None),
|
||||
status: Default::default(),
|
||||
recorder: Default::default(),
|
||||
tray: tray::Model::default(),
|
||||
capture_status: Default::default(),
|
||||
media: Default::default(),
|
||||
activities: Default::default(),
|
||||
|
||||
@@ -699,7 +699,7 @@ mod tests {
|
||||
))
|
||||
.push(
|
||||
cosmic::widget::button::custom(cosmic::widget::text("Action"))
|
||||
.name("Accessible action")
|
||||
.name(ferese_config::tr!("Accessible action"))
|
||||
.on_press(())
|
||||
.width(100)
|
||||
.height(40),
|
||||
|
||||
@@ -723,7 +723,7 @@ impl FereseShell {
|
||||
.spacing(3)
|
||||
.push(
|
||||
self.render
|
||||
.text("Notifications")
|
||||
.text(ferese_config::tr!("Notifications"))
|
||||
.size(16)
|
||||
.font(bold)
|
||||
.class(theme::Text::Color(primary)),
|
||||
@@ -767,7 +767,7 @@ impl FereseShell {
|
||||
.spacing(2)
|
||||
.push(
|
||||
self.render
|
||||
.text("Do Not Disturb")
|
||||
.text(ferese_config::tr!("Do Not Disturb"))
|
||||
.size(12)
|
||||
.font(bold)
|
||||
.class(theme::Text::Color(primary)),
|
||||
@@ -821,14 +821,14 @@ impl FereseShell {
|
||||
))
|
||||
.push(
|
||||
self.render
|
||||
.text("You're all caught up")
|
||||
.text(ferese_config::tr!("You're all caught up"))
|
||||
.size(15)
|
||||
.font(bold)
|
||||
.class(theme::Text::Color(primary)),
|
||||
)
|
||||
.push(
|
||||
self.render
|
||||
.text("New notifications will appear here.")
|
||||
.text(ferese_config::tr!("New notifications will appear here."))
|
||||
.size(11)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
@@ -843,7 +843,7 @@ impl FereseShell {
|
||||
.align_y(alignment::Vertical::Center)
|
||||
.push(
|
||||
self.render
|
||||
.text("Recent")
|
||||
.text(ferese_config::tr!("Recent"))
|
||||
.size(11)
|
||||
.font(bold)
|
||||
.class(theme::Text::Color(muted))
|
||||
|
||||
@@ -1101,7 +1101,7 @@ impl FereseShell {
|
||||
button::custom(
|
||||
icons::tinted(icons::morph::CLOSE, 18, icon_primary).opacity(opacity),
|
||||
)
|
||||
.name("Collapse panel hub")
|
||||
.name(ferese_config::tr!("Collapse panel hub"))
|
||||
.padding([5, 3])
|
||||
.width(30)
|
||||
.height(28)
|
||||
|
||||
@@ -149,7 +149,7 @@ impl FereseShell {
|
||||
return row![
|
||||
self.battery_indicator(self.config.theme, opacity, true),
|
||||
self.render
|
||||
.text("Low battery")
|
||||
.text(ferese_config::tr!("Low battery"))
|
||||
.size(13)
|
||||
.class(theme::Text::Color(foreground))
|
||||
.width(width)
|
||||
@@ -202,7 +202,7 @@ impl FereseShell {
|
||||
if expanded {
|
||||
strip = strip.push(
|
||||
self.render
|
||||
.text("Recording")
|
||||
.text(ferese_config::tr!("Recording"))
|
||||
.size(13)
|
||||
.class(theme::Text::Color(foreground))
|
||||
.width(Length::Fill),
|
||||
|
||||
@@ -9,6 +9,7 @@ pub(crate) struct Rendering {
|
||||
|
||||
impl Rendering {
|
||||
pub fn new(config: &crate::config::ShellConfig) -> Self {
|
||||
ferese_config::locale::set(config.ui.language);
|
||||
Self {
|
||||
font: crate::configured_font(config.font_family.as_deref()),
|
||||
motion: config.animations,
|
||||
|
||||
@@ -21,8 +21,14 @@ pub(super) fn view<'a>(
|
||||
opacity: p,
|
||||
} = style;
|
||||
if let Some(a) = &shell.status.audio {
|
||||
let mut volume_heading = row![shell.render.text("Volume").size(13).width(Length::Fill)]
|
||||
.align_y(Alignment::Center);
|
||||
let mut volume_heading = row![
|
||||
shell
|
||||
.render
|
||||
.text(ferese_config::tr!("Volume"))
|
||||
.size(13)
|
||||
.width(Length::Fill)
|
||||
]
|
||||
.align_y(Alignment::Center);
|
||||
if combined {
|
||||
volume_heading = volume_heading.push(shell_switch(
|
||||
shell.render,
|
||||
|
||||
@@ -146,6 +146,7 @@ impl FereseShell {
|
||||
|
||||
fn status_label(kind: Menu, s: &Snapshot) -> String {
|
||||
match kind {
|
||||
Menu::Tray => "Application tray".into(),
|
||||
Menu::Network => match &s.network {
|
||||
Some(n) if n.enabled => format!(
|
||||
"Wi-Fi: {}",
|
||||
@@ -202,6 +203,7 @@ pub(super) fn audio_icon(volume: u8, muted: bool) -> &'static [u8] {
|
||||
|
||||
pub(super) fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
|
||||
match kind {
|
||||
Menu::Tray => (ferese_theme::icons::APPLICATION, true),
|
||||
Menu::NightLight => (
|
||||
ferese_theme::icons::MOON,
|
||||
s.night_light.as_ref().is_some_and(|s| s.enabled),
|
||||
|
||||
@@ -73,7 +73,7 @@ pub(super) fn view<'a>(
|
||||
rows = rows.push(
|
||||
shell
|
||||
.render
|
||||
.text("Power Profiles service unavailable")
|
||||
.text(ferese_config::tr!("Power Profiles service unavailable"))
|
||||
.size(12)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
@@ -136,7 +136,7 @@ pub(super) fn brightness<'a>(
|
||||
} = style;
|
||||
if let Some(value) = shell.status.brightness {
|
||||
let brightness = column![
|
||||
shell.render.text("Brightness").size(13),
|
||||
shell.render.text(ferese_config::tr!("Brightness")).size(13),
|
||||
slider_row(
|
||||
shell.render,
|
||||
ferese_theme::icons::BRIGHTNESS,
|
||||
|
||||
@@ -50,7 +50,7 @@ pub(super) fn view<'a>(
|
||||
shell.render.text(device).size(14),
|
||||
shell
|
||||
.render
|
||||
.text("Connected")
|
||||
.text(ferese_config::tr!("Connected"))
|
||||
.size(12)
|
||||
.class(theme::Text::Color(muted)),
|
||||
]
|
||||
|
||||
@@ -18,9 +18,27 @@ pub(super) fn view<'a>(
|
||||
render.font,
|
||||
opacity,
|
||||
[
|
||||
menu_button(render, "‹", Message::CalendarMonth(-1), primary, opacity),
|
||||
menu_button(render, "Today", Message::CalendarToday, primary, opacity),
|
||||
menu_button(render, "›", Message::CalendarMonth(1), primary, opacity),
|
||||
menu_button(
|
||||
render,
|
||||
ferese_config::tr!("‹"),
|
||||
Message::CalendarMonth(-1),
|
||||
primary,
|
||||
opacity,
|
||||
),
|
||||
menu_button(
|
||||
render,
|
||||
ferese_config::tr!("Today"),
|
||||
Message::CalendarToday,
|
||||
primary,
|
||||
opacity,
|
||||
),
|
||||
menu_button(
|
||||
render,
|
||||
ferese_config::tr!("›"),
|
||||
Message::CalendarMonth(1),
|
||||
primary,
|
||||
opacity,
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ pub(super) fn level_meter(
|
||||
opacity: f32,
|
||||
) -> Element<'static, cosmic::Action<Message>> {
|
||||
let segment = move |amount: u16, filled: bool| {
|
||||
container(render.text(""))
|
||||
container(render.text(ferese_config::tr!("")))
|
||||
.width(Length::FillPortion(amount))
|
||||
.height(5)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
|
||||
@@ -79,7 +79,7 @@ pub(super) fn bar<'a>(
|
||||
width: Option<f32>,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let Some(player) = &shell.media.snapshot.selected else {
|
||||
return shell.render.text("").into();
|
||||
return shell.render.text(ferese_config::tr!("")).into();
|
||||
};
|
||||
let foreground = color(if selected {
|
||||
palette.accent
|
||||
@@ -302,7 +302,7 @@ pub(super) fn hub<'a>(
|
||||
return rows.push(
|
||||
shell
|
||||
.render
|
||||
.text("No active player")
|
||||
.text(ferese_config::tr!("No active player"))
|
||||
.size(14)
|
||||
.class(theme::Text::Color(style.muted)),
|
||||
);
|
||||
@@ -624,7 +624,7 @@ pub(super) fn view<'a>(
|
||||
rows = rows.push(
|
||||
shell
|
||||
.render
|
||||
.text("No active player")
|
||||
.text(ferese_config::tr!("No active player"))
|
||||
.size(13)
|
||||
.class(theme::Text::Color(style.muted)),
|
||||
);
|
||||
|
||||
@@ -39,6 +39,7 @@ struct MenuStyle {
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Menu {
|
||||
Tray,
|
||||
NightLight,
|
||||
Activities,
|
||||
Workspaces,
|
||||
@@ -90,7 +91,8 @@ impl Menu {
|
||||
}
|
||||
|
||||
fn title(self) -> &'static str {
|
||||
match self {
|
||||
ferese_config::locale::text(match self {
|
||||
Self::Tray => "Application tray",
|
||||
Self::NightLight => "Night light",
|
||||
Self::Activities => "Activities",
|
||||
Self::Workspaces => "Workspaces",
|
||||
@@ -105,12 +107,12 @@ impl Menu {
|
||||
Self::CaptureStatus => "Capture status",
|
||||
Self::Media => "Now playing",
|
||||
Self::Notifications => "Notifications",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn available(self, status: &Snapshot) -> bool {
|
||||
match self {
|
||||
Self::NightLight => true,
|
||||
Self::Tray | Self::NightLight => true,
|
||||
Self::Network => status.network.is_some(),
|
||||
Self::Bluetooth => status.bluetooth.is_some(),
|
||||
Self::Audio => status.audio.is_some(),
|
||||
@@ -207,7 +209,7 @@ impl FereseShell {
|
||||
{
|
||||
rows = rows.push(toggle_row(
|
||||
self.render,
|
||||
"Wi-Fi",
|
||||
ferese_config::tr!("Wi-Fi"),
|
||||
network.enabled,
|
||||
Action::Wifi(!network.enabled),
|
||||
theme,
|
||||
@@ -219,7 +221,7 @@ impl FereseShell {
|
||||
{
|
||||
rows = rows.push(toggle_row(
|
||||
self.render,
|
||||
"Mute",
|
||||
ferese_config::tr!("Mute"),
|
||||
audio.muted,
|
||||
Action::Mute(!audio.muted),
|
||||
theme,
|
||||
@@ -227,6 +229,7 @@ impl FereseShell {
|
||||
));
|
||||
}
|
||||
let mut rows = match kind {
|
||||
Menu::Tray => rows.push(self.tray_menu()),
|
||||
Menu::System => system::view(self, rows, style, true),
|
||||
Menu::NightLight => night_light::view(self, rows, style),
|
||||
Menu::Network => network::view(self, rows, style),
|
||||
@@ -245,14 +248,14 @@ impl FereseShell {
|
||||
if self.notifications.entries.is_empty() {
|
||||
rows = rows.push(
|
||||
self.render
|
||||
.text("No notifications")
|
||||
.text(ferese_config::tr!("No notifications"))
|
||||
.size(13)
|
||||
.class(theme::Text::Color(style.muted)),
|
||||
);
|
||||
} else {
|
||||
rows = rows.push(menu_button(
|
||||
self.render,
|
||||
"Clear notifications",
|
||||
ferese_config::tr!("Clear notifications"),
|
||||
Message::ClearNotifications,
|
||||
style.muted,
|
||||
opacity,
|
||||
@@ -269,7 +272,7 @@ impl FereseShell {
|
||||
.width(Length::Fill),
|
||||
menu_button(
|
||||
self.render,
|
||||
"×",
|
||||
ferese_config::tr!("×"),
|
||||
Message::RemoveNotification(notice.id),
|
||||
style.muted,
|
||||
opacity
|
||||
@@ -322,7 +325,7 @@ impl FereseShell {
|
||||
if !self.capture_status.active() {
|
||||
rows = rows.push(
|
||||
self.render
|
||||
.text("No active screen sharing or OBS output")
|
||||
.text(ferese_config::tr!("No active screen sharing or OBS output"))
|
||||
.size(13)
|
||||
.class(theme::Text::Color(style.muted)),
|
||||
);
|
||||
@@ -349,7 +352,7 @@ impl FereseShell {
|
||||
if !kind.available(&self.status) {
|
||||
rows = rows.push(
|
||||
self.render
|
||||
.text("Service unavailable")
|
||||
.text(ferese_config::tr!("Service unavailable"))
|
||||
.size(13)
|
||||
.class(theme::Text::Color(style.muted)),
|
||||
);
|
||||
@@ -378,7 +381,7 @@ impl FereseShell {
|
||||
|
||||
fn view_status_menu(&self) -> Element<'_, cosmic::Action<Message>> {
|
||||
let Some(menu) = &self.menu else {
|
||||
return self.render.text("").into();
|
||||
return self.render.text(ferese_config::tr!("")).into();
|
||||
};
|
||||
motion::frame_driven(
|
||||
menu.motion.revision(),
|
||||
@@ -405,7 +408,7 @@ impl FereseShell {
|
||||
|
||||
fn view_status_menu_at(&self, now: Instant) -> Element<'_, cosmic::Action<Message>> {
|
||||
let Some(menu) = &self.menu else {
|
||||
return self.render.text("").into();
|
||||
return self.render.text(ferese_config::tr!("")).into();
|
||||
};
|
||||
if menu.kind == Menu::Notifications && self.notifications.ready {
|
||||
return cosmic::widget::autosize::autosize(
|
||||
@@ -461,6 +464,7 @@ impl FereseShell {
|
||||
rows = rows.push(heading);
|
||||
}
|
||||
rows = match kind {
|
||||
Menu::Tray => rows.push(self.tray_menu()),
|
||||
Menu::System => system::view(self, rows, style, false),
|
||||
Menu::NightLight => night_light::view(self, rows, style),
|
||||
Menu::Network => network::view(self, rows, style),
|
||||
@@ -480,7 +484,7 @@ impl FereseShell {
|
||||
if kind != Menu::Calendar && !kind.available(&self.status) {
|
||||
rows = rows.push(
|
||||
self.render
|
||||
.text("Service unavailable")
|
||||
.text(ferese_config::tr!("Service unavailable"))
|
||||
.size(13)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ pub(super) fn view<'a>(
|
||||
row![
|
||||
shell
|
||||
.render
|
||||
.text("Signal strength")
|
||||
.text(ferese_config::tr!("Signal strength"))
|
||||
.size(12)
|
||||
.width(Length::Fill),
|
||||
shell.render.text(strength).size(12)
|
||||
|
||||
@@ -8,7 +8,12 @@ pub(super) fn view<'a>(
|
||||
style: MenuStyle,
|
||||
) -> MenuRows<'a> {
|
||||
let Some(status) = &shell.status.night_light else {
|
||||
return rows.push(shell.render.text("Night light is unavailable").size(13));
|
||||
return rows.push(
|
||||
shell
|
||||
.render
|
||||
.text(ferese_config::tr!("Night light is unavailable"))
|
||||
.size(13),
|
||||
);
|
||||
};
|
||||
rows = rows.push(toggle_row(
|
||||
shell.render,
|
||||
@@ -29,7 +34,10 @@ pub(super) fn view<'a>(
|
||||
))
|
||||
.step(100u16)
|
||||
.on_release(cosmic::Action::App(Message::CommitNightLightTemperature)),
|
||||
shell.render.text("Lower temperatures are warmer").size(12),
|
||||
shell
|
||||
.render
|
||||
.text(ferese_config::tr!("Lower temperatures are warmer"))
|
||||
.size(12),
|
||||
]
|
||||
.spacing(8);
|
||||
rows = rows.push(control_card(
|
||||
@@ -42,14 +50,16 @@ pub(super) fn view<'a>(
|
||||
rows = rows.push(
|
||||
shell
|
||||
.render
|
||||
.text("No display supports night light")
|
||||
.text(ferese_config::tr!("No display supports night light"))
|
||||
.size(13),
|
||||
);
|
||||
} else if status.enabled && status.failed_outputs > 0 {
|
||||
rows = rows.push(
|
||||
shell
|
||||
.render
|
||||
.text("Night light could not be applied to some displays")
|
||||
.text(ferese_config::tr!(
|
||||
"Night light could not be applied to some displays"
|
||||
))
|
||||
.size(12),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ pub(super) fn view<'a>(
|
||||
)
|
||||
.opacity(p),
|
||||
)
|
||||
.name("Open Ferese Settings")
|
||||
.name(ferese_config::tr!("Open Ferese Settings"))
|
||||
.width(30)
|
||||
.height(28)
|
||||
.padding([5, 3])
|
||||
@@ -67,7 +67,10 @@ pub(super) fn view<'a>(
|
||||
);
|
||||
modes = modes.push(cosmic::widget::tooltip(
|
||||
settings,
|
||||
shell.render.text("Open Ferese Settings").size(12),
|
||||
shell
|
||||
.render
|
||||
.text(ferese_config::tr!("Open Ferese Settings"))
|
||||
.size(12),
|
||||
cosmic::widget::tooltip::Position::Left,
|
||||
));
|
||||
}
|
||||
@@ -129,7 +132,7 @@ pub(super) fn view<'a>(
|
||||
.center_y(28),
|
||||
shell
|
||||
.render
|
||||
.text("Battery")
|
||||
.text(ferese_config::tr!("Battery"))
|
||||
.size(11)
|
||||
.width(Length::Fill)
|
||||
.height(16)
|
||||
@@ -281,7 +284,7 @@ pub(super) fn heading<'a>(
|
||||
color_with_opacity(theme.accent, p),
|
||||
))
|
||||
.padding(6)
|
||||
.name("Open Settings")
|
||||
.name(ferese_config::tr!("Open Settings"))
|
||||
.on_press(cosmic::Action::App(Message::Control(Action::Settings))),
|
||||
primary,
|
||||
false,
|
||||
|
||||
@@ -621,7 +621,7 @@ impl FereseShell {
|
||||
|
||||
pub(super) fn view_wallpaper(&self) -> Element<'_, cosmic::Action<Message>> {
|
||||
let Some(handle) = self.wallpaper.as_ref() else {
|
||||
return container(self.render.text(""))
|
||||
return container(self.render.text(ferese_config::tr!("")))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.class(theme::Container::custom(wallpaper_fallback_style))
|
||||
|
||||
@@ -508,10 +508,10 @@ impl FereseShell {
|
||||
|
||||
fn view_system_modal(&self, id: window::Id) -> Element<'_, cosmic::Action<Message>> {
|
||||
let Some(modal) = &self.system_modal else {
|
||||
return self.render.text("").into();
|
||||
return self.render.text(ferese_config::tr!("")).into();
|
||||
};
|
||||
let Some(surface) = modal.surfaces.iter().find(|surface| surface.id == id) else {
|
||||
return self.render.text("").into();
|
||||
return self.render.text(ferese_config::tr!("")).into();
|
||||
};
|
||||
motion::frame_driven(
|
||||
modal.motion.revision(),
|
||||
@@ -542,10 +542,10 @@ impl FereseShell {
|
||||
now: Instant,
|
||||
) -> Element<'_, cosmic::Action<Message>> {
|
||||
let Some(modal) = &self.system_modal else {
|
||||
return self.render.text("").into();
|
||||
return self.render.text(ferese_config::tr!("")).into();
|
||||
};
|
||||
let Some(surface) = modal.surfaces.iter().find(|surface| surface.id == id) else {
|
||||
return self.render.text("").into();
|
||||
return self.render.text(ferese_config::tr!("")).into();
|
||||
};
|
||||
let progress = modal.motion.progress_at(now);
|
||||
let content: Element<'_, cosmic::Action<Message>> = if surface.primary {
|
||||
@@ -568,7 +568,9 @@ impl FereseShell {
|
||||
{
|
||||
rows = rows.push(
|
||||
self.render
|
||||
.text("Applications requested that this action be prevented:")
|
||||
.text(ferese_config::tr!(
|
||||
"Applications requested that this action be prevented:"
|
||||
))
|
||||
.size(13)
|
||||
.class(theme::Text::Color(palette.muted)),
|
||||
);
|
||||
@@ -664,13 +666,13 @@ impl FereseShell {
|
||||
.saturating_sub(360)
|
||||
.clamp(40, 400) as f32;
|
||||
column![
|
||||
self.render.text("Welcome to Ferese").size(23),
|
||||
self.render.text(ferese_config::tr!("Welcome to Ferese")).size(23),
|
||||
ferese_theme::menus::section_label("Your active shortcuts", self.render.font, palette.muted,)
|
||||
.size(14),
|
||||
container(cosmic::widget::scrollable(bindings).height(Length::Shrink)).max_height(height),
|
||||
self.render
|
||||
.text(
|
||||
"Disable this guide in Settings → Shortcuts. It appears at each login until disabled."
|
||||
ferese_config::tr!("Disable this guide in Settings → Shortcuts. It appears at each login until disabled.")
|
||||
)
|
||||
.size(12)
|
||||
.class(theme::Text::Color(palette.muted)),
|
||||
@@ -718,7 +720,7 @@ impl FereseShell {
|
||||
}))
|
||||
.into()
|
||||
} else {
|
||||
self.render.text("").into()
|
||||
self.render.text(ferese_config::tr!("")).into()
|
||||
};
|
||||
let content = if surface.primary {
|
||||
motion::animated(
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
//! StatusNotifierItem host on the session bus. The watcher stays subscribed when
|
||||
//! icons are hidden so applications reappear immediately when enabled again.
|
||||
use crate::{FereseShell, Message, status_ui};
|
||||
use cosmic::{
|
||||
Element,
|
||||
app::Task,
|
||||
iced::{Alignment, Length, Subscription},
|
||||
widget::{button, column, container, icon, image, row},
|
||||
};
|
||||
use ferese_config::locale::text as t;
|
||||
use std::sync::Arc;
|
||||
use system_tray::{
|
||||
client::{ActivateRequest, Client},
|
||||
item::{Status, StatusNotifierItem},
|
||||
menu::MenuItem,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Event {
|
||||
Snapshot(
|
||||
Arc<Client>,
|
||||
Vec<(
|
||||
String,
|
||||
StatusNotifierItem,
|
||||
Option<system_tray::menu::TrayMenu>,
|
||||
)>,
|
||||
),
|
||||
Disconnected,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Input {
|
||||
Locate(String, cosmic::iced::window::Id, cosmic::widget::Id),
|
||||
Located(
|
||||
String,
|
||||
cosmic::iced::window::Id,
|
||||
Option<cosmic::iced::Rectangle>,
|
||||
),
|
||||
Activate(String),
|
||||
Context(
|
||||
String,
|
||||
cosmic::iced::window::Id,
|
||||
cosmic::iced::Rectangle<i32>,
|
||||
),
|
||||
Menu(String, i32),
|
||||
Manage,
|
||||
Done(Result<(), String>),
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct Model {
|
||||
client: Option<Arc<Client>>,
|
||||
items: Vec<Entry>,
|
||||
current: Option<String>,
|
||||
expanded: std::collections::HashSet<i32>,
|
||||
}
|
||||
struct Entry {
|
||||
address: String,
|
||||
item: StatusNotifierItem,
|
||||
menu: Option<system_tray::menu::TrayMenu>,
|
||||
pixmap: Option<image::Handle>,
|
||||
}
|
||||
impl Model {
|
||||
pub fn update(&mut self, event: Event) {
|
||||
match event {
|
||||
Event::Snapshot(client, items) => {
|
||||
self.client = Some(client);
|
||||
self.items = items
|
||||
.into_iter()
|
||||
.map(|(address, item, menu)| {
|
||||
let pixmap = pixmap(&item);
|
||||
Entry {
|
||||
address,
|
||||
item,
|
||||
menu,
|
||||
pixmap,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
self.items.sort_by(|a, b| a.item.id.cmp(&b.item.id));
|
||||
}
|
||||
Event::Disconnected => {
|
||||
self.client = None;
|
||||
self.items.clear();
|
||||
self.current = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn available(&self, config: &ferese_config::status::Tray) -> bool {
|
||||
self.visible(config).next().is_some()
|
||||
}
|
||||
fn visible<'a>(
|
||||
&'a self,
|
||||
config: &'a ferese_config::status::Tray,
|
||||
) -> impl Iterator<Item = &'a Entry> {
|
||||
self.items.iter().filter(move |e| {
|
||||
config.enabled
|
||||
&& (config.show_passive || e.item.status != Status::Passive)
|
||||
&& !config.hidden_items.contains(&e.item.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
pub fn subscription() -> Subscription<Event> {
|
||||
Subscription::run(|| {
|
||||
cosmic::iced::stream::channel(16, async move |mut out| {
|
||||
use cosmic::iced::futures::SinkExt;
|
||||
loop {
|
||||
if let Ok(client) = Client::new().await {
|
||||
let client = Arc::new(client);
|
||||
let mut receiver = client.subscribe();
|
||||
loop {
|
||||
let items = client
|
||||
.items()
|
||||
.lock()
|
||||
.map(|m| {
|
||||
m.iter()
|
||||
.map(|(a, (i, m))| (a.clone(), i.clone(), m.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if out
|
||||
.send(Event::Snapshot(client.clone(), items))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
match receiver.recv().await {
|
||||
Ok(_) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
if out.send(Event::Disconnected).await.is_err() {
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
fn pixmap(item: &StatusNotifierItem) -> Option<image::Handle> {
|
||||
let data = if item.status == Status::NeedsAttention {
|
||||
item.attention_icon_pixmap
|
||||
.as_ref()
|
||||
.or(item.icon_pixmap.as_ref())
|
||||
} else {
|
||||
item.icon_pixmap.as_ref()
|
||||
}?;
|
||||
let p = data
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.width > 0
|
||||
&& p.height > 0
|
||||
&& p.width <= 512
|
||||
&& p.height <= 512
|
||||
&& p.pixels.len() == p.width as usize * p.height as usize * 4
|
||||
})
|
||||
.min_by_key(|p| (p.width - 32).abs())?;
|
||||
let mut rgba = p.pixels.clone();
|
||||
for p in rgba.chunks_exact_mut(4) {
|
||||
p.rotate_left(1)
|
||||
}
|
||||
Some(image::Handle::from_rgba(
|
||||
p.width as u32,
|
||||
p.height as u32,
|
||||
rgba,
|
||||
))
|
||||
}
|
||||
impl FereseShell {
|
||||
pub(crate) fn view_tray(
|
||||
&self,
|
||||
id: cosmic::iced::window::Id,
|
||||
) -> Element<'_, cosmic::Action<Message>> {
|
||||
let size = self.config.status.tray.icon_size;
|
||||
let mut icons = row([]).spacing(4).align_y(Alignment::Center);
|
||||
for e in self.tray.visible(&self.config.status.tray) {
|
||||
let visual: Element<'_, cosmic::Action<Message>> = if let Some(p) = &e.pixmap {
|
||||
image(p.clone()).width(size).height(size).into()
|
||||
} else if let Some(name) = e.item.icon_name.as_ref().filter(|n| !n.is_empty()) {
|
||||
icon(icon::from_name(name.clone()).size(size as u16).handle())
|
||||
.size(size as u16)
|
||||
.into()
|
||||
} else {
|
||||
self.render.text(ferese_config::tr!("●")).size(size).into()
|
||||
};
|
||||
let address = e.address.clone();
|
||||
let title = e.item.title.as_deref().unwrap_or(&e.item.id);
|
||||
let control = button::custom(container(visual).center_y(size + 8.))
|
||||
.padding([0, 3])
|
||||
.name(title.to_owned())
|
||||
.on_press_with_rectangle(move |offset, bounds| {
|
||||
cosmic::Action::App(Message::TrayInput(Input::Context(
|
||||
address.clone(),
|
||||
id,
|
||||
cosmic::iced::Rectangle {
|
||||
x: (bounds.x - offset.x) as i32,
|
||||
y: (bounds.y - offset.y) as i32,
|
||||
width: bounds.width as i32,
|
||||
height: bounds.height as i32,
|
||||
},
|
||||
)))
|
||||
});
|
||||
// Left click activates normal applications; menu-only indicators open their menu.
|
||||
let control = if !e.item.item_is_menu {
|
||||
control.on_press(cosmic::Action::App(Message::TrayInput(Input::Activate(
|
||||
e.address.clone(),
|
||||
))))
|
||||
} else {
|
||||
control
|
||||
};
|
||||
let address = e.address.clone();
|
||||
let target = cosmic::widget::Id::unique();
|
||||
let control = cosmic::widget::mouse_area(container(control).id(target.clone()))
|
||||
.on_right_press(cosmic::Action::App(Message::TrayInput(Input::Locate(
|
||||
address, id, target,
|
||||
))));
|
||||
// An ordinary tooltip is clipped by a layer bar and can cover the
|
||||
// icon. Keep its accessible name; details live in the popover.
|
||||
icons = icons.push(control);
|
||||
}
|
||||
icons.into()
|
||||
}
|
||||
pub(crate) fn tray_input(&mut self, input: Input) -> Task<Message> {
|
||||
match input {
|
||||
Input::Locate(address, parent, target) => {
|
||||
cosmic::iced::advanced::widget::operate(FindBounds {
|
||||
target,
|
||||
parent,
|
||||
window: None,
|
||||
found: None,
|
||||
})
|
||||
.map(move |rect| {
|
||||
cosmic::Action::App(Message::TrayInput(Input::Located(
|
||||
address.clone(),
|
||||
parent,
|
||||
rect,
|
||||
)))
|
||||
})
|
||||
}
|
||||
Input::Located(address, parent, Some(rect)) => self.tray_input(Input::Context(
|
||||
address,
|
||||
parent,
|
||||
cosmic::iced::Rectangle {
|
||||
x: rect.x as i32,
|
||||
y: rect.y as i32,
|
||||
width: rect.width as i32,
|
||||
height: rect.height as i32,
|
||||
},
|
||||
)),
|
||||
Input::Located(_, _, None) => Task::none(),
|
||||
Input::Context(address, parent, mut rect) => {
|
||||
self.tray.current = Some(address.clone());
|
||||
self.tray.expanded.clear();
|
||||
if rect.x == 0 && rect.y == 0 {
|
||||
rect.x = 20;
|
||||
rect.y = 20;
|
||||
}
|
||||
let about = self.tray.client.clone().zip(
|
||||
self.tray
|
||||
.items
|
||||
.iter()
|
||||
.find(|i| i.address == address)
|
||||
.and_then(|i| i.item.menu.clone()),
|
||||
);
|
||||
let task = if let Some((client, path)) = about {
|
||||
cosmic::task::future(async move {
|
||||
Message::TrayInput(Input::Done(
|
||||
client
|
||||
.about_to_show_menuitem(address, path, 0)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string()),
|
||||
))
|
||||
})
|
||||
} else {
|
||||
Task::none()
|
||||
};
|
||||
Task::batch([
|
||||
task,
|
||||
self.open_menu(
|
||||
status_ui::Menu::Tray,
|
||||
status_ui::PopoverAnchor {
|
||||
parent,
|
||||
panel: self.panel_for_bar(parent).id.clone(),
|
||||
item: None,
|
||||
rectangle: rect,
|
||||
},
|
||||
),
|
||||
])
|
||||
}
|
||||
Input::Manage => {
|
||||
let command = self
|
||||
.config
|
||||
.status
|
||||
.settings_command
|
||||
.clone()
|
||||
.unwrap_or_else(|| vec!["ferese-settings".into()]);
|
||||
if let Some(exe) = command.first() {
|
||||
let _ = std::process::Command::new(exe)
|
||||
.args(&command[1..])
|
||||
.args(["--page", "tray"])
|
||||
.spawn();
|
||||
}
|
||||
self.close_menu()
|
||||
}
|
||||
Input::Activate(address) => {
|
||||
if let Some(client) = self.tray.client.clone() {
|
||||
cosmic::task::future(async move {
|
||||
Message::TrayInput(Input::Done(
|
||||
client
|
||||
.activate(ActivateRequest::Default {
|
||||
address,
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string()),
|
||||
))
|
||||
})
|
||||
} else {
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
Input::Menu(address, id) => {
|
||||
if self
|
||||
.tray
|
||||
.items
|
||||
.iter()
|
||||
.filter(|e| e.address == address)
|
||||
.filter_map(|e| e.menu.as_ref())
|
||||
.flat_map(|m| m.submenus.iter())
|
||||
.any(|m| contains_submenu(m, id))
|
||||
{
|
||||
if !self.tray.expanded.remove(&id) {
|
||||
self.tray.expanded.insert(id);
|
||||
}
|
||||
if let Some((client, path)) = self.tray.client.clone().zip(
|
||||
self.tray
|
||||
.items
|
||||
.iter()
|
||||
.find(|e| e.address == address)
|
||||
.and_then(|e| e.item.menu.clone()),
|
||||
) {
|
||||
return cosmic::task::future(async move {
|
||||
Message::TrayInput(Input::Done(
|
||||
client
|
||||
.about_to_show_menuitem(address, path, id)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string()),
|
||||
))
|
||||
});
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
let menu_path = self
|
||||
.tray
|
||||
.items
|
||||
.iter()
|
||||
.find(|e| e.address == address)
|
||||
.and_then(|e| e.item.menu.clone());
|
||||
if let Some((client, menu_path)) = self.tray.client.clone().zip(menu_path) {
|
||||
let task = cosmic::task::future(async move {
|
||||
Message::TrayInput(Input::Done(
|
||||
client
|
||||
.activate(ActivateRequest::MenuItem {
|
||||
address,
|
||||
menu_path,
|
||||
submenu_id: id,
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string()),
|
||||
))
|
||||
});
|
||||
Task::batch([task, self.close_menu()])
|
||||
} else {
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
Input::Done(result) => {
|
||||
if let Err(e) = result {
|
||||
eprintln!("tray action: {e}");
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) fn tray_menu(&self) -> Element<'_, cosmic::Action<Message>> {
|
||||
let mut rows = column([]).spacing(6);
|
||||
if let Some(e) = self
|
||||
.tray
|
||||
.current
|
||||
.as_ref()
|
||||
.and_then(|address| self.tray.items.iter().find(|e| &e.address == address))
|
||||
{
|
||||
if let Some(menu) = &e.menu {
|
||||
for item in &menu.submenus {
|
||||
rows = rows.push(self.tray_menu_item(&e.address, item, 0));
|
||||
}
|
||||
}
|
||||
rows = rows.push(
|
||||
button::custom(self.render.text(t("Manage tray icons")).size(12))
|
||||
.padding(8)
|
||||
.on_press(cosmic::Action::App(Message::TrayInput(Input::Manage))),
|
||||
);
|
||||
}
|
||||
rows.into()
|
||||
}
|
||||
fn tray_menu_item<'a>(
|
||||
&'a self,
|
||||
address: &str,
|
||||
item: &'a MenuItem,
|
||||
depth: usize,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
if !item.visible || depth > 8 {
|
||||
return cosmic::widget::Space::new().height(0).into();
|
||||
}
|
||||
let title = item
|
||||
.label
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.replace("__", "\0")
|
||||
.replace('_', "")
|
||||
.replace('\0', "_");
|
||||
let mut rows = column([]).spacing(3);
|
||||
rows = rows.push(
|
||||
button::custom(
|
||||
self.render
|
||||
.text(format!(
|
||||
"{}{}",
|
||||
title,
|
||||
if item.submenu.is_empty()
|
||||
&& item.children_display.as_deref() != Some("submenu")
|
||||
{
|
||||
""
|
||||
} else {
|
||||
" ›"
|
||||
}
|
||||
))
|
||||
.size(13),
|
||||
)
|
||||
.padding(8)
|
||||
.width(Length::Fill)
|
||||
.on_press_maybe(item.enabled.then(|| {
|
||||
cosmic::Action::App(Message::TrayInput(Input::Menu(address.into(), item.id)))
|
||||
})),
|
||||
);
|
||||
if self.tray.expanded.contains(&item.id) {
|
||||
for sub in &item.submenu {
|
||||
rows = rows
|
||||
.push(container(self.tray_menu_item(address, sub, depth + 1)).padding([0, 12]));
|
||||
}
|
||||
}
|
||||
rows.into()
|
||||
}
|
||||
}
|
||||
fn contains_submenu(item: &MenuItem, id: i32) -> bool {
|
||||
(item.id == id
|
||||
&& (!item.submenu.is_empty() || item.children_display.as_deref() == Some("submenu")))
|
||||
|| item.submenu.iter().any(|s| contains_submenu(s, id))
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn lazy_submenus_are_detected_without_children() {
|
||||
let item = system_tray::menu::MenuItem {
|
||||
id: 7,
|
||||
children_display: Some("submenu".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(super::contains_submenu(&item, 7));
|
||||
assert!(!super::contains_submenu(&item, 8));
|
||||
}
|
||||
#[test]
|
||||
fn argb_conversion_keeps_alpha() {
|
||||
let mut argb = [128, 2, 3, 4];
|
||||
argb.rotate_left(1);
|
||||
assert_eq!(argb, [2, 3, 4, 128]);
|
||||
}
|
||||
}
|
||||
|
||||
struct FindBounds {
|
||||
target: cosmic::widget::Id,
|
||||
parent: cosmic::iced::window::Id,
|
||||
window: Option<cosmic::iced::window::Id>,
|
||||
found: Option<cosmic::iced::Rectangle>,
|
||||
}
|
||||
impl cosmic::iced::advanced::widget::Operation<Option<cosmic::iced::Rectangle>> for FindBounds {
|
||||
fn traverse(
|
||||
&mut self,
|
||||
children: &mut dyn FnMut(
|
||||
&mut dyn cosmic::iced::advanced::widget::Operation<Option<cosmic::iced::Rectangle>>,
|
||||
),
|
||||
) {
|
||||
children(self);
|
||||
}
|
||||
fn set_window_id(&mut self, id: cosmic::iced::window::Id) {
|
||||
self.window = Some(id);
|
||||
}
|
||||
fn container(&mut self, id: Option<&cosmic::widget::Id>, bounds: cosmic::iced::Rectangle) {
|
||||
if self.window == Some(self.parent) && id == Some(&self.target) {
|
||||
self.found = Some(bounds);
|
||||
}
|
||||
}
|
||||
fn finish(
|
||||
&self,
|
||||
) -> cosmic::iced::advanced::widget::operation::Outcome<Option<cosmic::iced::Rectangle>> {
|
||||
cosmic::iced::advanced::widget::operation::Outcome::Some(self.found)
|
||||
}
|
||||
}
|
||||
@@ -329,7 +329,7 @@ impl FereseShell {
|
||||
.iter()
|
||||
.find(|workspace| workspace.id == hover.workspace)
|
||||
}) else {
|
||||
return self.render.text("").into();
|
||||
return self.render.text(ferese_config::tr!("")).into();
|
||||
};
|
||||
let active = self
|
||||
.snapshot
|
||||
|
||||
Reference in new issue
Block a user