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]
|
||||
|
||||
Reference in new issue
Block a user