Owner, 2026-10-19: hovering a scheme for a moment switches to it live; moving off goes to the next one hovered or back to the one in force, to sample schemes without re-opening the menu. A variant's row rested on for 50 ms is worn; off every row for 50 ms, the scheme in force again; the menu closed without a click, the one in force at once. Only a click chooses, and a preview is never kept. Tested with frames 10 ms apart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
5 files changed
+167
-9
No files matched your search
@@ -45,6 +45,10 @@ impl Shell<'_> {
|
||||
/// families out in columns.
|
||||
const FAMILY_ROW: f32 = 34.0;
|
||||
|
||||
/// How long the pointer rests on a scheme's row (or off every row) before
|
||||
/// that scheme (or the one in force) is worn, seconds.
|
||||
const PREVIEW_AFTER: f64 = 0.05;
|
||||
|
||||
/// The width of a variant's row in a family's flyout: room for its colors
|
||||
/// and the longest name.
|
||||
const SCHEME_ROW_WIDTH: f32 = 200.0;
|
||||
@@ -129,6 +133,12 @@ pub struct App {
|
||||
receipts: Receipts,
|
||||
/// The color scheme in force (schemes.rs).
|
||||
scheme: &'static Scheme,
|
||||
/// The scheme being worn: the one in force, or one previewed while
|
||||
/// its row in the menu is hovered.
|
||||
shown: &'static Scheme,
|
||||
/// The scheme the pointer asks for, and since when: worn once it has
|
||||
/// asked for PREVIEW_AFTER.
|
||||
wanted: Option<(&'static Scheme, f64)>,
|
||||
/// Which schemes its menu shows.
|
||||
filter: Filter,
|
||||
state: PageState,
|
||||
@@ -171,6 +181,8 @@ impl App {
|
||||
nav: Nav::new(Route::Overview),
|
||||
receipts: Receipts::new(ctx),
|
||||
scheme,
|
||||
shown: scheme,
|
||||
wanted: None,
|
||||
filter: Filter::default(),
|
||||
state: PageState::default(),
|
||||
palette: Default::default(),
|
||||
@@ -194,11 +206,63 @@ impl App {
|
||||
self.scheme.key()
|
||||
}
|
||||
|
||||
/// The scheme being worn ("Family/Variant"): a previewed one while
|
||||
/// its row is hovered (tests).
|
||||
pub fn shown_scheme_key(&self) -> String {
|
||||
self.shown.key()
|
||||
}
|
||||
|
||||
fn choose(&mut self, ctx: &egui::Context, scheme: &'static Scheme) {
|
||||
self.scheme = scheme;
|
||||
self.wear(ctx, scheme);
|
||||
}
|
||||
|
||||
/// Wear `scheme`, in force or previewed.
|
||||
fn wear(&mut self, ctx: &egui::Context, scheme: &'static Scheme) {
|
||||
self.shown = scheme;
|
||||
self.wanted = None;
|
||||
theme::apply_scheme(ctx, scheme);
|
||||
}
|
||||
|
||||
/// The preview (owner, 2026-10-19: "when we hover over a scheme for
|
||||
/// some time switch to it live; when we switch out of it go to either
|
||||
/// the new thing we're hovering or back to the current one"). While
|
||||
/// the menu is open, the scheme whose row is hovered, else the one in
|
||||
/// force, is worn once the pointer has asked for it PREVIEW_AFTER
|
||||
/// without change; the menu closed, the one in force is worn at once.
|
||||
/// A preview is never kept: only a click chooses.
|
||||
fn preview(&mut self, ctx: &egui::Context, open: bool, hovered: Option<&'static Scheme>) {
|
||||
let want = match (open, hovered) {
|
||||
(true, Some(s)) => s,
|
||||
_ => self.scheme,
|
||||
};
|
||||
if std::ptr::eq(want, self.shown) {
|
||||
self.wanted = None;
|
||||
return;
|
||||
}
|
||||
if !open {
|
||||
self.wear(ctx, want);
|
||||
return;
|
||||
}
|
||||
let now = ctx.input(|i| i.time);
|
||||
match self.wanted {
|
||||
Some((s, since)) if std::ptr::eq(s, want) => {
|
||||
let waited = now - since;
|
||||
if waited >= PREVIEW_AFTER {
|
||||
self.wear(ctx, want);
|
||||
} else {
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f64(
|
||||
PREVIEW_AFTER - waited,
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.wanted = Some((want, now));
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f64(PREVIEW_AFTER));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The color scheme button and its menu: a filter (all, light, dark),
|
||||
/// then each family with a flyout of its variants. `compact`: the
|
||||
/// narrow rail's icon alone.
|
||||
@@ -219,7 +283,8 @@ impl App {
|
||||
let config = egui::containers::menu::MenuConfig::new()
|
||||
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside);
|
||||
let mut chosen = None;
|
||||
let (response, _) = egui::containers::menu::MenuButton::from_button(button)
|
||||
let mut hovered = None;
|
||||
let (response, open) = egui::containers::menu::MenuButton::from_button(button)
|
||||
.config(config)
|
||||
.ui(ui, |ui| {
|
||||
ui.set_min_width(200.0);
|
||||
@@ -273,9 +338,12 @@ impl App {
|
||||
);
|
||||
ui.menu_button(label, |ui| {
|
||||
for v in variants {
|
||||
if scheme_row(ui, v, std::ptr::eq(*v, self.scheme))
|
||||
.clicked()
|
||||
{
|
||||
let row =
|
||||
scheme_row(ui, v, std::ptr::eq(*v, self.scheme));
|
||||
if row.hovered() {
|
||||
hovered = Some(*v);
|
||||
}
|
||||
if row.clicked() {
|
||||
chosen = Some(*v);
|
||||
ui.close();
|
||||
}
|
||||
@@ -288,8 +356,9 @@ impl App {
|
||||
});
|
||||
});
|
||||
response.on_hover_text(format!("Color scheme: {}", self.scheme.label()));
|
||||
if let Some(v) = chosen {
|
||||
self.choose(ui.ctx(), v);
|
||||
match chosen {
|
||||
Some(v) => self.choose(ui.ctx(), v),
|
||||
None => self.preview(ui.ctx(), open.is_some(), hovered),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -204,6 +204,87 @@ fn every_family_is_on_screen_in_order_and_a_whole_row_chooses() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Owner, 2026-10-19: a scheme hovered in the menu for a moment is worn
|
||||
/// live; off its row, the next one hovered, or the one in force, is worn
|
||||
/// again; nothing is chosen until a click.
|
||||
#[test]
|
||||
fn a_hovered_scheme_is_shown_live_and_only_a_click_keeps_it() {
|
||||
let (_dir, source) = demo("schemes-preview");
|
||||
let books = data::load_once(&source).unwrap();
|
||||
// Frames 10 ms apart, to see the 50 ms wait.
|
||||
let mut h = Harness::builder()
|
||||
.with_size(egui::vec2(1440.0, 920.0))
|
||||
.with_step_dt(0.01)
|
||||
.build_eframe(|cc| App::with_books(&cc.egui_ctx, books, false, Route::Overview));
|
||||
h.run_steps(3);
|
||||
let page = |h: &Harness<'_, App>| h.ctx.global_style().visuals.panel_fill;
|
||||
let light = page(&h);
|
||||
h.get_by_label_contains("Wanderbooks Light").click();
|
||||
h.run_steps(3);
|
||||
family_row(&h, "Ayu").hover();
|
||||
h.run_steps(3);
|
||||
// A variant's row (the filter has a "Dark" too: the flyout's is last).
|
||||
let rest_on = |h: &mut Harness<'_, App>, name: &str| {
|
||||
let r = h.query_all_by_label(name).last().unwrap().rect();
|
||||
h.hover_at(egui::pos2(r.left() + 20.0, r.center().y));
|
||||
};
|
||||
rest_on(&mut h, "Mirage");
|
||||
h.run_steps(2);
|
||||
assert_eq!(
|
||||
h.state().shown_scheme_key(),
|
||||
"Wanderbooks/Light",
|
||||
"not before 50 ms"
|
||||
);
|
||||
h.run_steps(8);
|
||||
assert_eq!(h.state().shown_scheme_key(), "Ayu/Mirage");
|
||||
assert_eq!(
|
||||
h.state().scheme_key(),
|
||||
"Wanderbooks/Light",
|
||||
"only shown, not chosen"
|
||||
);
|
||||
let mirage = page(&h);
|
||||
assert_ne!(mirage, light);
|
||||
// On to the next one hovered.
|
||||
rest_on(&mut h, "Dark");
|
||||
h.run_steps(10);
|
||||
assert_eq!(h.state().shown_scheme_key(), "Ayu/Dark");
|
||||
// Off every row: back to the one in force.
|
||||
let filter = h.query_all_by_label("All").next().unwrap().rect();
|
||||
h.hover_at(filter.center());
|
||||
h.run_steps(10);
|
||||
assert_eq!(h.state().shown_scheme_key(), "Wanderbooks/Light");
|
||||
assert_eq!(page(&h), light);
|
||||
// Closed while one is shown: the one in force, at once; nothing kept.
|
||||
family_row(&h, "Ayu").hover();
|
||||
h.run_steps(3);
|
||||
rest_on(&mut h, "Mirage");
|
||||
h.run_steps(10);
|
||||
assert_eq!(h.state().shown_scheme_key(), "Ayu/Mirage");
|
||||
h.key_press(egui::Key::Escape);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().shown_scheme_key(), "Wanderbooks/Light");
|
||||
assert_eq!(h.state().scheme_key(), "Wanderbooks/Light");
|
||||
assert_eq!(page(&h), light);
|
||||
let mut prefs = Prefs::default();
|
||||
eframe::App::save(h.state_mut(), &mut prefs);
|
||||
assert_eq!(
|
||||
prefs.0["scheme"], "Wanderbooks/Light",
|
||||
"a preview is never kept"
|
||||
);
|
||||
// A click keeps it.
|
||||
h.get_by_label_contains("Wanderbooks Light").click();
|
||||
h.run_steps(3);
|
||||
family_row(&h, "Ayu").hover();
|
||||
h.run_steps(3);
|
||||
rest_on(&mut h, "Mirage");
|
||||
h.run_steps(10);
|
||||
h.get_by_label("Mirage").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().scheme_key(), "Ayu/Mirage");
|
||||
assert_eq!(h.state().shown_scheme_key(), "Ayu/Mirage");
|
||||
assert_eq!(page(&h), mirage);
|
||||
}
|
||||
|
||||
/// Pictures of the menu and of a few schemes, for a person to look at
|
||||
/// (`SHOT_DIR=… cargo test -p ledger-gui --test schemes -- --ignored`).
|
||||
#[test]
|
||||
|
||||
@@ -7,7 +7,11 @@ and the viewer's own two). The menu's filter narrows the list to light or
|
||||
dark schemes. Families are listed alphabetically, each one's variants too,
|
||||
in as many columns as the window's height needs, so every family is on
|
||||
screen at once; a variant's whole row (its colors and its name) chooses
|
||||
it.
|
||||
it. Resting the pointer on a variant's row for 50 ms wears it live, to
|
||||
sample schemes without choosing; off the rows, the next one hovered or the
|
||||
scheme in force is worn again, and closing the menu without a click puts
|
||||
back the one in force. Only a click chooses, and only a chosen scheme is
|
||||
kept.
|
||||
The choice is a viewer preference, kept with eframe's other preferences;
|
||||
nothing is written to the books, and no figure or word changes with the
|
||||
scheme (`schemes.rs`, `painted`).
|
||||
|
||||
+5
-1
@@ -452,7 +452,11 @@ menu, its scroll bar hidden), the families not alphabetical, and a click on
|
||||
a variant's colors doing nothing. Now the families are alphabetical, in
|
||||
columns with no scrolling, and a variant's whole row is its button;
|
||||
`every_family_is_on_screen_in_order_and_a_whole_row_chooses` checks all
|
||||
three in a tall window and a short one. Earlier: the viewer's own light palette's green below 4.5:1 on the
|
||||
three in a tall window and a short one. The live preview (owner, the same
|
||||
day) is checked by `a_hovered_scheme_is_shown_live_and_only_a_click_keeps_it`
|
||||
with frames 10 ms apart: not worn before 50 ms, worn after, the next row's
|
||||
scheme on moving, the one in force off the rows and at once on closing,
|
||||
nothing kept until a click. Earlier: the viewer's own light palette's green below 4.5:1 on the
|
||||
sidebar (4.35:1), and a first fix (moving colors toward the text color)
|
||||
that turned Everforest's red and green into the same grey; colors keep
|
||||
their hue and change only lightness.
|
||||
|
||||
+1
-1
@@ -247,7 +247,7 @@ The server, the CLI and the GUI find the data root with one shared function (`le
|
||||
- **Save a copy / Open with system app** on any document (`Snapshot::attachment_bytes`, snapshot.rs:328). Fixes the viewer half of defect 3.
|
||||
- **TIN click:** locally, the GUI prompts for the passphrase, reads the wrapped keyslot and the sealed record from the data root, unwraps in its own process, and returns the value to the clipboard-only path (`tests/tin_copy.rs` behaviour), then zeroizes. This is not access-logged, because the GUI never writes; the docs say so. Remotely, TINs appear only on `/h/` pages.
|
||||
- `[viewer] open_at_start` becomes a per-human eframe preference (`app.rs:833`), not journaled.
|
||||
- **Color schemes** (owner, 2026-10-19: "full blown colorscheme support … all the top neovim style colorschemes", "on the order of 20 families, each with a flyout of all its variants", a light/dark/all filter; Ayu included). The button in the sidebar's lower corner opens a menu: the filter, then, alphabetically and all on screen at once, the viewer's own family (Wanderbooks Light and Dark) and twenty families of editor schemes (Ayu, Catppuccin, Cyberdream, Dracula, Everforest, GitHub, Gruvbox, Kanagawa, Material, Melange, Monokai Pro, Night Owl, Nightfox, Nord, One Dark, Oxocarbon, Rosé Pine, Solarized, Sonokai, Tokyo Night), each a flyout of all its variants, alphabetical, each a row with a strip of its colors that chooses it. The choice and the filter are per-human eframe preferences like `open_at_start`; nothing is journaled and no figure changes (`schemes.rs` paints nine pages in all 80 schemes and checks every painted word and figure is the same, in the same place, and readable on what is under it). Every palette keeps every text color (figures, gains and losses, warnings, links, the faintest text) at WCAG AA contrast (4.5:1) on every background it is drawn on: a scheme's own color that falls short keeps its hue and only changes lightness, and gains, losses and text stay plainly different to the eye. The scheme pins egui's light or dark theme, so the system flipping changes nothing. The palettes come from each scheme's official source (`docs/color-schemes.md`).
|
||||
- **Color schemes** (owner, 2026-10-19: "full blown colorscheme support … all the top neovim style colorschemes", "on the order of 20 families, each with a flyout of all its variants", a light/dark/all filter; Ayu included). The button in the sidebar's lower corner opens a menu: the filter, then, alphabetically and all on screen at once, the viewer's own family (Wanderbooks Light and Dark) and twenty families of editor schemes (Ayu, Catppuccin, Cyberdream, Dracula, Everforest, GitHub, Gruvbox, Kanagawa, Material, Melange, Monokai Pro, Night Owl, Nightfox, Nord, One Dark, Oxocarbon, Rosé Pine, Solarized, Sonokai, Tokyo Night), each a flyout of all its variants, alphabetical, each a row with a strip of its colors that chooses it; a row rested on for 50 ms is worn live until the pointer moves on (owner, 2026-10-19), and only a click keeps it. The choice and the filter are per-human eframe preferences like `open_at_start`; nothing is journaled and no figure changes (`schemes.rs` paints nine pages in all 80 schemes and checks every painted word and figure is the same, in the same place, and readable on what is under it). Every palette keeps every text color (figures, gains and losses, warnings, links, the faintest text) at WCAG AA contrast (4.5:1) on every background it is drawn on: a scheme's own color that falls short keeps its hue and only changes lightness, and gains, losses and text stay plainly different to the eye. The scheme pins egui's light or dark theme, so the system flipping changes nothing. The palettes come from each scheme's official source (`docs/color-schemes.md`).
|
||||
|
||||
### 3.7 Backup and tamper evidence
|
||||
|
||||
|
||||
Reference in new issue
Block a user