Split shell, settings, and compositor into focused modules

This commit is contained in:
Samuel Onoja committed 2026-10-01 12:47:26 +01:00
1 parent 2b9f30fa5d
commit cbf048566a
38 files changed
+9998 -9817

No files matched your search

+247
View File
@@ -0,0 +1,247 @@
use cosmic::widget::column;
use super::{
Alignment, App, Element, Field, Kind, Length, Message, button, container, displays, fonts, row, set, slider,
text_input, visuals, widget,
};
impl App {
pub(super) fn label<'a>(
&self,
text: impl Into<std::borrow::Cow<'a, str>> + 'a,
size: f32,
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
ferese_theme::text(text, self.font).size(size)
}
pub(super) fn note(&self, text: &str) -> Element<'static, Message> {
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
self.label(text.to_owned(), 12.)
.class(cosmic::theme::Text::Color(palette.muted))
.into()
}
pub(super) fn field(&self, mut field: Field) -> Element<'static, Message> {
if field.path == "theme.material.opacity" {
let appearance = self.resolved.theme.appearance;
let name = match appearance {
ferese_config::theme::Appearance::Light => "light",
ferese_config::theme::Appearance::Dark => "dark",
};
field.path = format!("theme.{name}.material.opacity");
field.description = format!("Opacity for the current {name} appearance. Text and icons stay opaque.");
if let Kind::Range { default, .. } = &mut field.kind {
*default = self.resolved.theme.tokens.material.opacity;
}
}
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
let mut labels = column([]).spacing(3).push(self.label(field.label.clone(), 13.));
if !field.description.is_empty() {
labels = labels.push(
self.label(field.description.clone(), 11.)
.class(cosmic::theme::Text::Color(palette.muted)),
);
}
let labels = labels.width(Length::Fill);
let path = field.path.clone();
let control: Element<'static, Message> = match field.kind.clone() {
Kind::Font => {
let value = self
.inputs
.get(&path)
.cloned()
.unwrap_or_else(|| self.draft.string(&path, ""));
let families = fonts::families();
let selected = if value.is_empty() {
Some(0)
} else {
families.iter().position(|family| family == &value)
};
let selection_path = path.clone();
let commit = field.clone();
column([])
.spacing(6)
.push(
cosmic::iced::widget::pick_list(
families,
selected.map(|index| families[index].clone()),
move |family: String| {
let value = if family == "Default font" {
String::new()
} else {
family
};
Message::SelectFont(selection_path.clone(), value)
},
)
.font(self.font)
.text_size(12)
.width(225),
)
.push(
text_input("Default font", value)
.font(self.font)
.style(visuals::input_style(palette))
.on_input(move |value| Message::Draft(path.clone(), value))
.on_submit(move |_| Message::Commit(field.clone()))
.on_unfocus(Message::Commit(commit))
.width(225)
.size(12),
)
.into()
}
Kind::Toggle(default) => {
let enabled = self.draft.boolean(&path, default);
ferese_theme::controls::switch(enabled, palette)
.name(format!("{}: {}", field.label, if enabled { "on" } else { "off" }))
.on_press(Message::Change(set(&path, !enabled)))
.into()
}
Kind::Range {
default,
min,
max,
step,
suffix,
integer,
} => {
let value = self
.ranges
.get(&path)
.copied()
.unwrap_or_else(|| self.draft.number(&path, default));
let display = if integer || step >= 1. {
format!("{value:.0}{suffix}")
} else {
format!("{value:.2}{suffix}")
};
let release = field.clone();
row([])
.align_y(Alignment::Center)
.spacing(14)
.push(
slider(min..=max, value, move |value| Message::Range(field.clone(), value))
.step(step)
.class(ferese_theme::menus::slider(palette.accent, 1.))
.on_release(Message::Release(release))
.width(145),
)
.push(self.label(display, 12.).width(65))
.into()
}
Kind::Choice { default, choices } => {
let value = self.draft.string(&path, default);
let mut options = row([]).spacing(2);
for (key, label) in choices {
options = options.push(
button::custom(self.label(*label, 12.))
.padding([6, 9])
.class(visuals::button_style(palette, value == *key))
.on_press(Message::Change(set(&path, *key))),
);
}
container(options)
.padding(3)
.class(visuals::surface(palette.sidebar, 10.))
.into()
}
Kind::Text { default, argv } => {
let value = self.inputs.get(&path).cloned().unwrap_or_else(|| {
if argv {
self.draft.argv(&path)
} else {
self.draft.string(&path, default)
}
});
let commit = field.clone();
let is_color = path.starts_with("theme.colors.");
let swatch = visuals::color(&value, palette.accent);
let input = text_input(default, value)
.font(self.font)
.style(visuals::input_style(palette))
.on_input(move |value| Message::Draft(path.clone(), value))
.on_submit(move |_| Message::Commit(field.clone()))
.on_unfocus(Message::Commit(commit))
.width(if is_color { 155 } else { 225 })
.size(12);
if is_color {
row([])
.spacing(8)
.align_y(Alignment::Center)
.push(container(widget::Space::new().width(24).height(24)).class(visuals::surface(swatch, 6.)))
.push(input)
.into()
} else {
input.into()
}
}
};
container(
row([])
.spacing(16)
.align_y(Alignment::Center)
.push(labels)
.push(control),
)
.padding([12, 12])
.width(Length::Fill)
.class(visuals::surface(palette.card, 0.))
.into()
}
pub(super) fn refresh_controls(&self, prefix: &str) -> Element<'_, Message> {
let matcher = self.draft.string(&format!("{prefix}.match"), "");
let configured = self.draft.string(&format!("{prefix}.mode"), "");
let automatic = self.draft.boolean(&format!("{prefix}.auto_refresh"), false);
let Some(display) = self
.displays
.iter()
.find(|display| display.connector == matcher || display.identity == matcher)
else {
return self.note("Connect this display to choose a supported refresh rate.");
};
let modes = displays::choices(display, &configured);
let mut controls = row([]).spacing(8).align_y(Alignment::Center);
for mode in &modes {
let selected = !automatic
&& configured
.split('@')
.nth(1)
.and_then(|rate| rate.parse::<f64>().ok())
.is_some_and(|rate| (rate * 1000. - f64::from(mode.refresh)).abs() < 1.);
controls = controls.push(
ferese_theme::controls::text_button(
mode.label(),
self.font,
visuals::Palette::from_resolved(&self.resolved.presented),
selected,
)
.on_press(Message::RefreshRate(prefix.to_owned(), *mode, false)),
);
}
let supports_auto = modes.iter().any(|mode| (59_000..=61_000).contains(&mode.refresh));
if supports_auto && let Some(mode) = modes.last() {
controls = controls.push(
ferese_theme::controls::text_button(
"Auto",
self.font,
visuals::Palette::from_resolved(&self.resolved.presented),
automatic,
)
.on_press(Message::RefreshRate(prefix.to_owned(), *mode, true)),
);
}
let current = display.current.map(|mode| mode.label()).unwrap_or_else(|| "Off".into());
let profile = display.profile.as_deref().unwrap_or("automatic configuration");
column![
self.label("Refresh rate", 14.),
self.note(&format!("Current: {current} · active profile: {profile}")),
controls,
self.note(if supports_auto {
"Auto uses 60 Hz below 30% on battery. Normal refresh returns on AC or at 35%. Switching may briefly blank the display. Choosing a rate turns Auto off."
} else {
"This resolution has no supported 60 Hz mode for automatic switching."
}),
].spacing(8).into()
}
}