532 lines
25 KiB
Rust
532 lines
25 KiB
Rust
use super::{
|
||
Alignment, App, Element, Field, Kind, Length, Message, Page, button, column, container, row, schema, scrollable,
|
||
text_input, visuals, widget,
|
||
};
|
||
|
||
impl App {
|
||
pub(super) fn page_view(&self) -> Element<'_, Message> {
|
||
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||
let mut sidebar = column([])
|
||
.spacing(3)
|
||
.push(
|
||
widget::mouse_area(
|
||
row([])
|
||
.spacing(10)
|
||
.align_y(Alignment::Center)
|
||
.push(visuals::brand_icon())
|
||
.push(self.label("Ferese", 16.))
|
||
.width(Length::Fill),
|
||
)
|
||
.on_press(Message::DragWindow)
|
||
.interaction(cosmic::iced::mouse::Interaction::Grab),
|
||
)
|
||
.push(widget::Space::new().height(14))
|
||
.push(
|
||
text_input("Search settings", self.search.clone())
|
||
.on_input(Message::Search)
|
||
.font(self.font)
|
||
.style(visuals::input_style(palette))
|
||
.size(12),
|
||
)
|
||
.push(widget::Space::new().height(8));
|
||
for page in Page::ALL {
|
||
sidebar = sidebar.push(
|
||
button::custom(
|
||
row([])
|
||
.spacing(10)
|
||
.align_y(Alignment::Center)
|
||
.push(visuals::icon(
|
||
page,
|
||
if self.page == page {
|
||
palette.accent
|
||
} else {
|
||
palette.muted
|
||
},
|
||
))
|
||
.push(self.label(page.title(), 13.)),
|
||
)
|
||
.width(Length::Fill)
|
||
.padding([8, 8])
|
||
.class(visuals::navigation_style(palette, self.page == page))
|
||
.on_press(Message::Page(page)),
|
||
);
|
||
}
|
||
let sidebar = container(sidebar.push(widget::Space::new().height(Length::Fill)))
|
||
.width(204)
|
||
.height(Length::Fill)
|
||
.padding([20, 10])
|
||
.class(visuals::surface(palette.sidebar, 0.));
|
||
let mut heading = row([]).align_y(Alignment::Center).spacing(20).push(
|
||
column([])
|
||
.spacing(5)
|
||
.push(self.label(
|
||
if self.search.is_empty() {
|
||
self.page.title()
|
||
} else {
|
||
"Search"
|
||
},
|
||
24.,
|
||
))
|
||
.push(
|
||
self.label(
|
||
if self.search.is_empty() {
|
||
self.page.subtitle()
|
||
} else {
|
||
"Find a setting for your desktop."
|
||
},
|
||
12.,
|
||
)
|
||
.class(cosmic::theme::Text::Color(palette.muted)),
|
||
)
|
||
.width(Length::Fill),
|
||
);
|
||
if self.page == Page::Appearance && 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(
|
||
ferese_theme::controls::switch(automatic, palette)
|
||
.name(if automatic {
|
||
"Auto appearance: on"
|
||
} else {
|
||
"Auto appearance: off"
|
||
})
|
||
.on_press(Message::AutoAppearance(!automatic)),
|
||
),
|
||
);
|
||
let scheduled = self.draft.string("theme.schedule.source", "schedule") == "schedule";
|
||
let source = if scheduled { "Schedule" } else { "System" };
|
||
let label = if automatic {
|
||
format!(
|
||
"{source} · {}",
|
||
if self.resolved.theme.appearance == ferese_config::theme::Appearance::Light {
|
||
"Light"
|
||
} else {
|
||
"Dark"
|
||
},
|
||
)
|
||
} else {
|
||
source.to_owned()
|
||
};
|
||
let explanation = if scheduled {
|
||
format!(
|
||
"Auto uses Light from {} and Dark from {} ({})",
|
||
self.draft.string("theme.schedule.light_at", "07:00"),
|
||
self.draft.string("theme.schedule.dark_at", "19:00"),
|
||
self.draft.string("theme.schedule.timezone", "system"),
|
||
)
|
||
} else {
|
||
"Auto follows the GTK/GNOME appearance preference".to_owned()
|
||
};
|
||
heading = heading.push(widget::tooltip(
|
||
ferese_theme::controls::text_button(label, self.font, palette, self.auto_details)
|
||
.name("Automatic appearance settings")
|
||
.on_press(Message::AutoDetails(!self.auto_details)),
|
||
self.label(explanation, 12.),
|
||
widget::tooltip::Position::Bottom,
|
||
));
|
||
}
|
||
|
||
let mut body = column([]).spacing(12);
|
||
|
||
if !self.search.is_empty() {
|
||
if !Page::ALL.into_iter().any(|p| p.matches(&self.search)) {
|
||
body = body.push(self.note("No matching settings. Try wallpaper, keyboard, or motion."));
|
||
}
|
||
for page in Page::ALL.into_iter().filter(|p| p.matches(&self.search)) {
|
||
body = body.push(
|
||
button::custom(
|
||
column([])
|
||
.spacing(5)
|
||
.push(self.label(page.title(), 16.))
|
||
.push(self.label(page.subtitle(), 12.)),
|
||
)
|
||
.width(Length::Fill)
|
||
.padding(20)
|
||
.class(visuals::button_style(palette, false))
|
||
.on_press(Message::Page(page)),
|
||
);
|
||
}
|
||
} else {
|
||
if matches!(self.page, Page::Bar | Page::Windows) {
|
||
body = body.push(visuals::preview_resolved(&self.draft, &self.resolved.presented));
|
||
}
|
||
if self.page == Page::Appearance {
|
||
let split = visuals::split(&self.draft);
|
||
if split {
|
||
body = body.push(self.theme_gallery(Some(ferese_config::theme::Appearance::Light)));
|
||
body = body.push(self.theme_gallery(Some(ferese_config::theme::Appearance::Dark)));
|
||
} else {
|
||
body = body.push(self.theme_gallery(None));
|
||
}
|
||
body = body.push(
|
||
widget::checkbox(split)
|
||
.label("Use different themes for light and dark")
|
||
.text_size(12)
|
||
.font(self.font)
|
||
.on_toggle(Message::SplitThemes),
|
||
);
|
||
if let Some(note) = &self.resolved.fallback_note {
|
||
body = body.push(self.note(note));
|
||
}
|
||
}
|
||
|
||
if self.page == Page::Wallpaper {
|
||
if let Some(handle) = &self.thumbnail {
|
||
body = body.push(
|
||
widget::image(handle.clone())
|
||
.width(Length::Fill)
|
||
.height(190)
|
||
.content_fit(cosmic::iced::ContentFit::Contain),
|
||
);
|
||
} else {
|
||
body = body.push(self.note(if self.thumbnail_loading {
|
||
"Loading wallpaper preview…"
|
||
} else {
|
||
self.thumbnail_error
|
||
.as_deref()
|
||
.unwrap_or("Choose an image to preview your wallpaper.")
|
||
}));
|
||
}
|
||
body = body.push(button::standard("Choose image…").on_press(Message::PickWallpaper));
|
||
}
|
||
let fields = schema::fields(self.page);
|
||
|
||
if !fields.is_empty() {
|
||
let mut group = column([]).spacing(1);
|
||
if self.page == Page::Appearance && self.auto_details {
|
||
group = group.push(self.auto_controls());
|
||
}
|
||
for field in fields {
|
||
if self.page == Page::Appearance
|
||
&& (field.path == "theme.mode"
|
||
|| field.path.starts_with("theme.schedule.")
|
||
|| field.path.ends_with(".file"))
|
||
{
|
||
continue;
|
||
}
|
||
group = group.push(self.field(field));
|
||
}
|
||
if self.page == Page::Appearance {
|
||
group = group.push(
|
||
button::custom(
|
||
row([])
|
||
.spacing(8)
|
||
.align_y(Alignment::Center)
|
||
.push(self.label("Advanced customization", 13.))
|
||
.push(visuals::action_icon(
|
||
if self.advanced_theme {
|
||
"M6 15l6-6 6 6"
|
||
} else {
|
||
"M6 9l6 6 6-6"
|
||
},
|
||
palette.muted,
|
||
)),
|
||
)
|
||
.name(if self.advanced_theme {
|
||
"Collapse advanced customization"
|
||
} else {
|
||
"Expand advanced customization"
|
||
})
|
||
.padding(12)
|
||
.width(Length::Fill)
|
||
.class(visuals::button_style(palette, false))
|
||
.on_press(Message::AdvancedTheme(!self.advanced_theme)),
|
||
);
|
||
if self.advanced_theme {
|
||
group = group.push(self.theme_file_picker());
|
||
}
|
||
}
|
||
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||
}
|
||
|
||
match self.page {
|
||
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(button::standard("Preview lock screen").on_press(Message::PreviewLock));
|
||
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."));
|
||
}
|
||
Page::Desktop => {
|
||
let can_change_list = !self.saving && !self.note_editors.values().any(|e| e.dirty);
|
||
body = body.push(self.label("Sticky notes", 16.));
|
||
body = body.push(self.note("Edit here; notes save after you pause typing. Desktop cards stay behind windows and are click-through."));
|
||
for index in 0..self.draft.records("desktop_widgets.notes") {
|
||
let id = self.draft.string(&format!("desktop_widgets.notes.{index}.id"), "note");
|
||
let mut group = column([]).spacing(8);
|
||
|
||
for field in schema::note_fields(index) {
|
||
group = group.push(self.field(field));
|
||
}
|
||
|
||
if let Some(editor) = self.note_editors.get(&id) {
|
||
let id = id.clone();
|
||
group = group.push(
|
||
widget::TextEditor::new(&editor.content)
|
||
.height(160)
|
||
.font(self.font)
|
||
.size(14.)
|
||
.on_action(move |action| Message::NoteAction(id.clone(), action)),
|
||
);
|
||
}
|
||
group = group.push(button::destructive("Remove note").on_press_maybe(
|
||
can_change_list.then_some(Message::Remove("desktop_widgets.notes".into(), index)),
|
||
));
|
||
body = body.push(container(group).padding(12).class(visuals::surface(palette.card, 14.)));
|
||
}
|
||
body = body.push(
|
||
button::standard("Add note").on_press_maybe(
|
||
(can_change_list && self.draft.records("desktop_widgets.notes") < 32)
|
||
.then_some(Message::AddNote),
|
||
),
|
||
);
|
||
}
|
||
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."));
|
||
|
||
for index in 0..self.draft.records("autostart") {
|
||
let prefix = format!("autostart.{index}");
|
||
let group = column([])
|
||
.spacing(1)
|
||
.push(self.field(Field::new(
|
||
format!("{prefix}.command"),
|
||
"Program",
|
||
"A program and its arguments, not a shell script.",
|
||
Kind::Text {
|
||
default: "",
|
||
argv: true,
|
||
},
|
||
)))
|
||
.push(self.field(Field::new(
|
||
format!("{prefix}.enabled"),
|
||
"Open at login",
|
||
"",
|
||
Kind::Toggle(true),
|
||
)))
|
||
.push(self.field(Field::new(
|
||
format!("{prefix}.restart"),
|
||
"Restart if it exits",
|
||
"Managed by the Ferese session.",
|
||
Kind::Toggle(true),
|
||
)))
|
||
.push(
|
||
container(button::destructive("Remove item").on_press_maybe(
|
||
(!self.saving).then_some(Message::Remove("autostart".into(), index)),
|
||
))
|
||
.padding(12),
|
||
);
|
||
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||
}
|
||
body = body.push(
|
||
row([])
|
||
.spacing(10)
|
||
.push(
|
||
text_input("Program and arguments", self.new_command.clone())
|
||
.on_input(Message::NewCommand),
|
||
)
|
||
.push(button::standard("Add login item").on_press_maybe(
|
||
(!self.saving && !self.new_command.trim().is_empty()).then_some(Message::AddCommand),
|
||
)),
|
||
);
|
||
}
|
||
Page::Shortcuts => {
|
||
body = body.push(self.note("Keyboard and swipe bindings share the same actions. Use Swipe3Up, Swipe3Down, Swipe3Left, or Swipe3Right as a shortcut (also supports 4 or 5 fingers). Actions include toggle-overview, spawn, focus, move, workspace-next, workspace-previous, workspace-back-and-forth, and none. Leave Argument empty for actions such as toggle-overview or none."));
|
||
let mut gestures = row([]).spacing(6);
|
||
for (keys, label) in [
|
||
("Swipe3Up", "Customize swipe up"),
|
||
("Swipe3Down", "Customize swipe down"),
|
||
("Swipe3Left", "Customize swipe left"),
|
||
("Swipe3Right", "Customize swipe right"),
|
||
] {
|
||
let exists = (0..self.draft.records("bindings")).any(|index| {
|
||
self.draft
|
||
.string(&format!("bindings.{index}.keys"), "")
|
||
.eq_ignore_ascii_case(keys)
|
||
});
|
||
|
||
if !exists {
|
||
gestures = gestures.push(
|
||
button::standard(label)
|
||
.on_press_maybe((!self.saving).then_some(Message::AddSwipe(keys))),
|
||
);
|
||
}
|
||
}
|
||
|
||
body = body.push(gestures);
|
||
|
||
for index in 0..self.draft.records("bindings") {
|
||
let prefix = format!("bindings.{index}");
|
||
let group = column([])
|
||
.spacing(1)
|
||
.push(self.field(schema::text(
|
||
format!("{prefix}.keys"),
|
||
"Shortcut",
|
||
"For example: Super+Return or Swipe3Up",
|
||
"",
|
||
)))
|
||
.push(self.field(schema::text(
|
||
format!("{prefix}.action"),
|
||
"Action",
|
||
"Action such as toggle-overview, spawn, focus, move, or none.",
|
||
"",
|
||
)))
|
||
.push(self.field(schema::text(
|
||
format!("{prefix}.argument"),
|
||
"Argument",
|
||
"Command name, direction, or action argument.",
|
||
"",
|
||
)));
|
||
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||
}
|
||
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."));
|
||
}
|
||
}
|
||
Page::Displays => {
|
||
body = body.push(self.note("Display profiles update connected outputs live. Keep your config open for adding profiles or changing display positions."));
|
||
for profile in 0..self.draft.records("output_profiles") {
|
||
let prefix = format!("output_profiles.{profile}");
|
||
body =
|
||
body.push(self.label(self.draft.string(&format!("{prefix}.name"), "Display profile"), 17.));
|
||
|
||
for output in 0..self.draft.records(&format!("{prefix}.outputs")) {
|
||
let prefix = format!("{prefix}.outputs.{output}");
|
||
let group = column([])
|
||
.spacing(1)
|
||
.push(self.refresh_controls(&prefix))
|
||
.push(self.field(schema::text(
|
||
format!("{prefix}.match"),
|
||
"Display",
|
||
"Output name or matching pattern.",
|
||
"",
|
||
)))
|
||
.push(self.field(schema::text(
|
||
format!("{prefix}.mode"),
|
||
"Resolution",
|
||
"For example: 2560x1440@60. Leave unchanged to retain automatic selection.",
|
||
"",
|
||
)))
|
||
.push(self.field(schema::range(
|
||
format!("{prefix}.scale"),
|
||
"Scale",
|
||
"Logical size of text and controls.",
|
||
1.,
|
||
0.75,
|
||
3.,
|
||
0.25,
|
||
"×",
|
||
false,
|
||
)));
|
||
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||
}
|
||
}
|
||
if self.draft.records("output_profiles") == 0 {
|
||
body =
|
||
body.push(self.note(
|
||
"No saved profiles. Ferese currently configures connected displays automatically.",
|
||
));
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
let mut content = column([]).spacing(16).push(
|
||
widget::mouse_area(heading.width(Length::Fill))
|
||
.on_press(Message::DragWindow)
|
||
.interaction(cosmic::iced::mouse::Interaction::Grab),
|
||
);
|
||
|
||
if let Some(error) = &self.error {
|
||
content = content.push(
|
||
container(self.label(error.clone(), 12.))
|
||
.padding(12)
|
||
.width(Length::Fill)
|
||
.class(visuals::surface(palette.error, 10.)),
|
||
);
|
||
}
|
||
|
||
content = content.push(
|
||
scrollable(
|
||
container(body)
|
||
.padding(cosmic::iced::Padding {
|
||
top: 0.,
|
||
right: 12.,
|
||
bottom: 0.,
|
||
left: 2.,
|
||
})
|
||
.width(Length::Fill),
|
||
)
|
||
.direction(cosmic::iced::widget::scrollable::Direction::Vertical(
|
||
cosmic::iced::widget::scrollable::Scrollbar::new()
|
||
.width(10)
|
||
.scroller_width(3),
|
||
))
|
||
.height(Length::Fill),
|
||
);
|
||
let footer = row([])
|
||
.spacing(4)
|
||
.align_y(Alignment::Center)
|
||
.push(widget::tooltip(
|
||
visuals::action_icon(
|
||
if self.saving {
|
||
"M12 3v9l5 3 M21 12a9 9 0 1 1-9-9"
|
||
} else {
|
||
"M9 12l2 2 4-4 M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0"
|
||
},
|
||
palette.muted,
|
||
),
|
||
self.label(if self.saving { "Saving…" } else { &self.status }, 11.),
|
||
widget::tooltip::Position::Top,
|
||
))
|
||
.push(widget::Space::new().width(Length::Fill))
|
||
.push(widget::tooltip(
|
||
button::custom(visuals::action_icon(
|
||
"M3 10h8 M3 10V3 M3 10c3-7 17-6 17 3a7 7 0 0 1-7 7",
|
||
if self.undo.is_some() && !self.saving {
|
||
palette.text
|
||
} else {
|
||
palette.muted
|
||
},
|
||
))
|
||
.padding(4)
|
||
.on_press_maybe((self.undo.is_some() && !self.saving).then_some(Message::Undo)),
|
||
self.label("Undo last change", 11.),
|
||
widget::tooltip::Position::Top,
|
||
))
|
||
.push(widget::tooltip(
|
||
button::custom(visuals::action_icon(
|
||
"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",
|
||
palette.text,
|
||
))
|
||
.padding(4)
|
||
.on_press_maybe((!self.saving).then_some(Message::Reload)),
|
||
self.label("Reload configuration", 11.),
|
||
widget::tooltip::Position::Top,
|
||
));
|
||
row([])
|
||
.push(sidebar)
|
||
.push(
|
||
container(
|
||
container(column([]).push(content.height(Length::Fill)).push(footer).spacing(6))
|
||
.padding(cosmic::iced::Padding {
|
||
top: 20.,
|
||
right: 20.,
|
||
bottom: 8.,
|
||
left: 20.,
|
||
})
|
||
.max_width(840)
|
||
.width(Length::Fill)
|
||
.height(Length::Fill),
|
||
)
|
||
.width(Length::Fill)
|
||
.center_x(Length::Fill)
|
||
.height(Length::Fill)
|
||
.class(visuals::surface(palette.background, 0.)),
|
||
)
|
||
.into()
|
||
}
|
||
}
|