Model list arrow flips when open; temperatures in a fixed order; bigger avatar; clock font, weight and digit width as settings; Cancel at the top of the event form, Esc cancels it

This commit is contained in:
server2
2026-10-16 02:51:51 +02:00
parent d59a5bc81c
commit ed0eb532db
6 changed files with 60 additions and 19 deletions
+3 -1
View File
@@ -321,7 +321,9 @@ pub fn card(width: f32, height: f32) -> Rectangle {
let theme = theme::now(); let theme = theme::now();
// the model, as a chip that opens the list of the others // the model, as a chip that opens the list of the others
let mut buttons: Vec<Box<dyn Widget>> = vec![Box::new(pods::chip(&settings::model(), Some(Glyph::Chevron), pick))]; // the model, as a chip that opens the list of the others; its arrow points up while the list is open
let arrow = if ai.models.is_some() { Glyph::ChevronUp } else { Glyph::Chevron };
let mut buttons: Vec<Box<dyn Widget>> = vec![Box::new(pods::chip(&settings::model(), Some(arrow), pick))];
if ai.busy { if ai.busy {
buttons.push(Box::new(pods::chip("Stop", None, stop))); buttons.push(Box::new(pods::chip("Stop", None, stop)));
+5 -2
View File
@@ -518,7 +518,7 @@ settings! {
interval_ms: u64 = 1000, 200..=10000; "How often the system monitor reads the system" interval_ms: u64 = 1000, 200..=10000; "How often the system monitor reads the system"
history: usize = 60, 10..=600; "How many readings the graphs show" history: usize = 60, 10..=600; "How many readings the graphs show"
graph_height: f32 = 34.0, 10..=200; "Height of each graph" graph_height: f32 = 34.0, 10..=200; "Height of each graph"
temperatures: usize = 6, 0..=20; "How many temperature sensors to show, hottest first" temperatures: usize = 6, 0..=20; "How many temperature sensors to show, always in the same order: CPU, iGPU, eGPU, NVMe, memory, board, then the rest"
warm: f32 = 65.0, 20..=120; "From this temperature (°C) a sensor shows in the warm colour" warm: f32 = 65.0, 20..=120; "From this temperature (°C) a sensor shows in the warm colour"
hot: f32 = 85.0, 20..=130; "From this temperature (°C) a sensor shows in the hot colour" hot: f32 = 85.0, 20..=130; "From this temperature (°C) a sensor shows in the hot colour"
cool_color: Color = Color::rgb(120, 200, 255); "Colour of a sensor below the warm temperature" cool_color: Color = Color::rgb(120, 200, 255); "Colour of a sensor below the warm temperature"
@@ -532,9 +532,12 @@ settings! {
home { home {
width: f32 = 820.0, 300..=1600; "Width of the home card. It opens rightwards from beside the island, so on a 1920 px wide monitor about 820 fits" width: f32 = 820.0, 300..=1600; "Width of the home card. It opens rightwards from beside the island, so on a 1920 px wide monitor about 820 fits"
clock_size: f32 = 64.0, 20..=200; "Size of the big clock" clock_size: f32 = 64.0, 20..=200; "Size of the big clock"
clock_font: String = String::new(); "The big clock's font family, any installed one (fc-list : family lists them); empty takes the shell's font"
clock_weight: u64 = 700, 100..=900; "The big clock's weight, 100 (thin) to 900 (black); a font without that weight shows its nearest one"
clock_letter_width: f32 = 0.0, 0..=2; "Width of one of the clock's digits as a share of its size, so the weather beside it makes room; 0 takes the shell font's"
seconds: bool = false; "Show seconds on the big clock" seconds: bool = false; "Show seconds on the big clock"
avatar: String = String::from("~/.face"); "The picture shown as the user's avatar; a drawn person without one" avatar: String = String::from("~/.face"); "The picture shown as the user's avatar; a drawn person without one"
avatar_size: f32 = 48.0, 24..=120; "Size of the avatar; the greeting row is as tall" avatar_size: f32 = 84.0, 24..=200; "Size of the avatar; the greeting row is as tall"
gap: f32 = 12.0, 0..=60; "Space between the home card's parts" gap: f32 = 12.0, 0..=60; "Space between the home card's parts"
city: String = String::from("Munich"); "The weather's place, as shown" city: String = String::from("Munich"); "The weather's place, as shown"
latitude: f32 = 48.137, -90.0..=90.0; "The weather's latitude" latitude: f32 = 48.137, -90.0..=90.0; "The weather's latitude"
+2
View File
@@ -12,6 +12,7 @@ pub enum Glyph {
Muted, Muted,
Bluetooth, Bluetooth,
Chevron, Chevron,
ChevronUp,
// pointing left, for a menu's way back // pointing left, for a menu's way back
Back, Back,
Note, Note,
@@ -138,6 +139,7 @@ pub fn glyph_shapes(glyph: Glyph, size: f32, color: Color, weight: f32, origin:
.cap(Cap::Round) .cap(Cap::Round)
], ],
Glyph::Chevron => shapes![Path::new().move_to(x(0.12), y(0.32)).line_to(x(0.5), y(0.7)).line_to(x(0.88), y(0.32)).stroke(1.5, color).cap(Cap::Round)], Glyph::Chevron => shapes![Path::new().move_to(x(0.12), y(0.32)).line_to(x(0.5), y(0.7)).line_to(x(0.88), y(0.32)).stroke(1.5, color).cap(Cap::Round)],
Glyph::ChevronUp => shapes![Path::new().move_to(x(0.12), y(0.68)).line_to(x(0.5), y(0.3)).line_to(x(0.88), y(0.68)).stroke(1.5, color).cap(Cap::Round)],
Glyph::Back => shapes![Path::new().move_to(x(0.64), y(0.14)).line_to(x(0.3), y(0.5)).line_to(x(0.64), y(0.86)).stroke(thin.max(1.6), color).cap(Cap::Round)], Glyph::Back => shapes![Path::new().move_to(x(0.64), y(0.14)).line_to(x(0.3), y(0.5)).line_to(x(0.64), y(0.86)).stroke(thin.max(1.6), color).cap(Cap::Round)],
// an eighth note: round head, stem, a curved flag // an eighth note: round head, stem, a curved flag
Glyph::Note => shapes![ Glyph::Note => shapes![
+36 -11
View File
@@ -350,6 +350,22 @@ fn sync_settings() -> (String, String, String, bool) {
} }
// whether the card has a form to type into: then it keeps the keyboard and stays open // whether the card has a form to type into: then it keeps the keyboard and stays open
// the event form closes without saving
fn cancel() {
Agenda::write().form = None;
}
// Esc in the card: closes the event form if it's open (then true), else leaves it to the card
pub fn escape() -> bool {
let open = Agenda::read().form.is_some();
if open {
cancel();
}
open
}
pub fn typing() -> bool { pub fn typing() -> bool {
Agenda::read().form.is_some() Agenda::read().form.is_some()
} }
@@ -1197,18 +1213,18 @@ fn top_row(clock: &Clock, width: f32, shapes: &mut Vec<Box<dyn Shape>>) -> Box<d
// the big clock, and the weather beside it; `y` is where the row starts in the card // the big clock, and the weather beside it; `y` is where the row starts in the card
fn middle_row(clock: &Clock, forecast: &Forecast, width: f32, today: i64, y: f32, shapes: &mut Vec<Box<dyn Shape>>) -> Box<dyn Widget> { fn middle_row(clock: &Clock, forecast: &Forecast, width: f32, today: i64, y: f32, shapes: &mut Vec<Box<dyn Shape>>) -> Box<dyn Widget> {
let theme = theme::now();
let gap = settings::gap(); let gap = settings::gap();
let seconds_today = clock.now.rem_euclid(86_400); let seconds_today = clock.now.rem_euclid(86_400);
let (hour, minute, second) = (seconds_today / 3_600, seconds_today % 3_600 / 60, seconds_today % 60); let (hour, minute, second) = (seconds_today / 3_600, seconds_today % 3_600 / 60, seconds_today % 60);
let time = if settings::seconds() { format!("{hour:02}:{minute:02}:{second:02}") } else { format!("{hour:02}:{minute:02}") }; let time = if settings::seconds() { format!("{hour:02}:{minute:02}:{second:02}") } else { format!("{hour:02}:{minute:02}") };
let clock_w = (settings::clock_size() * font::letter_width() * time.chars().count() as f32).min(width / 2.0); let letter = if settings::clock_letter_width() > 0.0 { settings::clock_letter_width() } else { font::letter_width() };
let clock_w = (settings::clock_size() * letter * time.chars().count() as f32).min(width / 2.0);
let weather_w = (width - clock_w - gap).max(0.0); let weather_w = (width - clock_w - gap).max(0.0);
let height = middle_h(); let height = middle_h();
let big = Rectangle::new().width(clock_w).height(height).align_child(Start, Center).child(Text::new(time).size(settings::clock_size()).weight(Weight::Bold).color(theme.text).tight()); let big = Rectangle::new().width(clock_w).height(height).align_child(Start, Center).child(clock_text(time));
let weather = weather(forecast, weather_w, today, (clock_w + gap, y + (height - weather_h()) / 2.0), shapes); let weather = weather(forecast, weather_w, today, (clock_w + gap, y + (height - weather_h()) / 2.0), shapes);
Box::new( Box::new(
@@ -1219,6 +1235,14 @@ fn middle_row(clock: &Clock, forecast: &Forecast, width: f32, today: i64, y: f32
) )
} }
// the big clock's digits in its own font and weight
fn clock_text(time: String) -> Text {
let digits = Text::new(time).size(settings::clock_size()).weight(settings::clock_weight().min(1000) as u16).color(theme::now().text).tight();
let family = settings::clock_font();
if family.trim().is_empty() { digits } else { digits.font(family.trim()) }
}
// the weather now over the coming days, at `origin` in the card // the weather now over the coming days, at `origin` in the card
fn weather(forecast: &Forecast, width: f32, today: i64, origin: (f32, f32), shapes: &mut Vec<Box<dyn Shape>>) -> Rectangle { fn weather(forecast: &Forecast, width: f32, today: i64, origin: (f32, f32), shapes: &mut Vec<Box<dyn Shape>>) -> Rectangle {
let theme = theme::now(); let theme = theme::now();
@@ -1744,6 +1768,7 @@ fn event_form(agenda: &Agenda, form: &Form, width: f32, origin: (f32, f32), shap
}; };
let mut top: Vec<Box<dyn Widget>> = vec![Box::new(Text::new(heading).size(text::heading()).weight(Weight::Bold).color(theme.text))]; let mut top: Vec<Box<dyn Widget>> = vec![Box::new(Text::new(heading).size(text::heading()).weight(Weight::Bold).color(theme.text))];
let mut actions: Vec<Box<dyn Widget>> = Vec::new();
if form.editing.is_some() { if form.editing.is_some() {
let words = match (form.confirm, series) { let words = match (form.confirm, series) {
@@ -1753,11 +1778,16 @@ fn event_form(agenda: &Agenda, form: &Form, width: f32, origin: (f32, f32), shap
}; };
let button = button_size(); let button = button_size();
let at = (origin.0 + width - choice_width(words, button / 2.0 + 6.0), origin.1 + (pods::header_height() - button) / 2.0); let cancel_w = choice_width("Cancel", 0.0);
let at = (origin.0 + width - cancel_w - gap - choice_width(words, button / 2.0 + 6.0), origin.1 + (pods::header_height() - button) / 2.0);
top.push(Box::new(icon_chip(words, form.confirm, Glyph::Trash, at, shapes, remove))); actions.push(Box::new(icon_chip(words, form.confirm, Glyph::Trash, at, shapes, remove)));
} }
// Cancel at the top, where it's always in sight (Esc does the same)
actions.push(Box::new(pods::chip("Cancel", None, cancel)));
top.push(Box::new(Row::new(actions).gap(gap).align(Center)));
let mut parts: Vec<Box<dyn Widget>> = vec![ let mut parts: Vec<Box<dyn Widget>> = vec![
Box::new(Rectangle::new().width(width).height(pods::header_height()).child(Row::new(top).width(width).justify(SpaceBetween).align(Center))), Box::new(Rectangle::new().width(width).height(pods::header_height()).child(Row::new(top).width(width).justify(SpaceBetween).align(Center))),
Box::new(field(title, width)), Box::new(field(title, width)),
@@ -1834,12 +1864,7 @@ fn event_form(agenda: &Agenda, form: &Form, width: f32, origin: (f32, f32), shap
parts.push(Box::new(wrapped(problem.clone(), overlays::low_battery(), width))); parts.push(Box::new(wrapped(problem.clone(), overlays::low_battery(), width)));
} }
let buttons: Vec<Box<dyn Widget>> = vec![ parts.push(Box::new(choice(if form.saving { "Saving…" } else { "Save" }, true, None, save)));
Box::new(choice(if form.saving { "Saving…" } else { "Save" }, true, None, save)),
Box::new(pods::chip("Cancel", None, || Agenda::write().form = None)),
];
parts.push(Box::new(Row::new(buttons).gap(gap).align(Center)));
Box::new(Rectangle::new().width(width).height(calendar_h()).child(Column::new(parts).gap(gap))) Box::new(Rectangle::new().width(width).height(calendar_h()).child(Column::new(parts).gap(gap)))
} }
+2 -1
View File
@@ -334,7 +334,8 @@ fn side(monitor: &Monitor, left: bool) -> LayerWindow {
let window = if typing { let window = if typing {
window.keyboard(Keyboard::OnDemand).on_key(|key| { window.keyboard(Keyboard::OnDemand).on_key(|key| {
if key == Key::Escape { // Esc closes the home card's event form first, then the card
if key == Key::Escape && !home::escape() {
close(); close();
} }
}) })
+12 -4
View File
@@ -156,12 +156,19 @@ fn gpu_clock() -> (f32, String) {
(0.0, String::from("no clock to read")) (0.0, String::from("no clock to read"))
} }
// every hwmon device's hottest sensor, named for what it is, hottest first // what the temperatures are shown in, always: a sensor keeps its place whatever its reading
const ORDER: [&str; 8] = ["CPU", "iGPU", "eGPU", "NVMe", "Memory", "Board", "ThinkPad", "Wi-Fi"];
// every hwmon device's hottest sensor, named for what it is, in ORDER (then by name, then by device)
fn temperatures() -> Vec<(String, Glyph, f32)> { fn temperatures() -> Vec<(String, Glyph, f32)> {
let mut found: Vec<(String, Glyph, f32)> = Vec::new(); let mut found: Vec<(String, Glyph, f32)> = Vec::new();
for entry in fs::read_dir("/sys/class/hwmon").into_iter().flatten().flatten() { // the devices in their numbers' order (hwmon2 before hwmon10), so two of a kind keep theirs too
let device = entry.path(); let mut devices: Vec<PathBuf> = fs::read_dir("/sys/class/hwmon").into_iter().flatten().flatten().map(|entry| entry.path()).collect();
devices.sort_by_key(|path| path.file_name().and_then(|name| name.to_str()).and_then(|name| name.trim_start_matches("hwmon").parse::<u32>().ok()).unwrap_or(u32::MAX));
for device in devices {
let kind = read(device.join("name")).trim().to_string(); let kind = read(device.join("name")).trim().to_string();
let hottest = fs::read_dir(&device) let hottest = fs::read_dir(&device)
@@ -191,7 +198,8 @@ fn temperatures() -> Vec<(String, Glyph, f32)> {
found.push((name.to_string(), glyph, celsius)); found.push((name.to_string(), glyph, celsius));
} }
found.sort_by(|a, b| b.2.total_cmp(&a.2)); // a stable sort: same kinds keep the devices' order
found.sort_by_key(|(name, ..)| (ORDER.iter().position(|kind| kind == name).unwrap_or(ORDER.len()), name.clone()));
found.truncate(settings::temperatures()); found.truncate(settings::temperatures());
// a kind shown more than once is numbered, in the order shown // a kind shown more than once is numbered, in the order shown