mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-25 02:30:15 -06:00
LR-LIB-RATING: one Tab stop per rating row, visible-only favourite registration, true docs
The star row is a single Tab stop (the current rating's star, or the first) with the arrow keys stepping inside it and the focus following. favorite_button registers its automation id only while on screen again. The module and test docs no longer mention a size/read-only API that doesn't exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e161744d70
commit
8720bbcfef
@@ -4,12 +4,13 @@
|
||||
//! badges, the compare caption and the filter bar.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! if let Some(r) = Rating::new("bottom", photo.rating).size(RatingSize::Large).show(ui) { … }
|
||||
//! Rating::new("row", 3).size(RatingSize::Small).read_only().show(ui);
|
||||
//! if let Some(r) = Rating::new("bottom", photo.rating).show(ui) { … }
|
||||
//! paint_rating(ui.painter(), pos, 3, RatingSize::Small, color);
|
||||
//! ```
|
||||
//!
|
||||
//! Hovering a star previews the rating a click would set; clicking the current rating clears it.
|
||||
//! With a star focused, ← and → lower and raise the rating (0 to 5).
|
||||
//! The row is one Tab stop (the star of the current rating, or the first when unrated); inside it, ← and →
|
||||
//! lower and raise the rating (0 to 5) and the focus follows.
|
||||
|
||||
use egui::{Color32, Key, Modifiers, Painter, Pos2, Rect, Sense, Ui, pos2, vec2};
|
||||
|
||||
@@ -120,6 +121,10 @@ impl Rating {
|
||||
}
|
||||
}
|
||||
|
||||
fn star_id(&self, ui: &Ui, i: u8) -> egui::Id {
|
||||
ui.id().with(("rating-star", &self.id, i))
|
||||
}
|
||||
|
||||
/// Draw the row. Returns the new rating when a click or an arrow key changed it.
|
||||
pub fn show(self, ui: &mut Ui) -> Option<u8> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
@@ -128,8 +133,14 @@ impl Rating {
|
||||
let mut hover_n = None;
|
||||
let mut focused = false;
|
||||
let mut cells = [Rect::NOTHING; STARS as usize];
|
||||
// One Tab stop for the row, like a radio group: the star of the current rating (the first when
|
||||
// unrated), plus whichever star already has the focus so it keeps it
|
||||
let stop = self.rating.max(1) - 1;
|
||||
for (i, slot) in (0..STARS).zip(cells.iter_mut()) {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(cell, cell), Sense::click());
|
||||
let id = self.star_id(ui, i);
|
||||
let (r, _) = ui.allocate_exact_size(vec2(cell, cell), Sense::hover());
|
||||
let sense = if i == stop || ui.memory(|m| m.has_focus(id)) { Sense::click() } else { Sense::CLICK };
|
||||
let resp = ui.interact(r, id, sense);
|
||||
announce_selected(&resp, format!("{} ★", i + 1), i < self.rating);
|
||||
register(ui.ctx(), self.widget_id(i + 1), r);
|
||||
if resp.hovered() {
|
||||
@@ -156,6 +167,11 @@ impl Rating {
|
||||
ui.memory_mut(|m| m.move_focus(egui::FocusDirection::None));
|
||||
}
|
||||
out = nudged_rating(self.rating, left, right);
|
||||
if let Some(n) = out {
|
||||
// keep the focus on the star that is now the row's Tab stop
|
||||
let to = self.star_id(ui, n.max(1) - 1);
|
||||
ui.memory_mut(|m| m.request_focus(to));
|
||||
}
|
||||
}
|
||||
// The hovered star also gets the icon-button hover square, so hovering the current rating (where
|
||||
// the preview looks the same) still shows feedback.
|
||||
@@ -178,7 +194,10 @@ pub fn favorite_button(ui: &mut Ui, widget: String, favorite: bool, name: &str)
|
||||
let side = ui.spacing().interact_size.y;
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(side, side), Sense::click());
|
||||
announce_selected(&resp, name, favorite);
|
||||
register(ui.ctx(), widget, r);
|
||||
// (rows scrolled out of view can't be clicked)
|
||||
if ui.is_rect_visible(r) {
|
||||
register(ui.ctx(), widget, r);
|
||||
}
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, 4.0, t.hover.gamma_multiply(0.7));
|
||||
}
|
||||
@@ -211,4 +230,21 @@ mod tests {
|
||||
assert_eq!(nudged_rating(3, false, true), Some(4));
|
||||
assert_eq!(nudged_rating(3, true, true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_scrolled_out_of_view_are_not_registered() {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(300.0, 100.0))), ..Default::default() }, |ui| {
|
||||
egui::ScrollArea::vertical().max_height(60.0).show(ui, |ui| {
|
||||
for i in 0..30 {
|
||||
favorite_button(ui, format!("effectStar:{i}"), false, "Favorite");
|
||||
}
|
||||
});
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
let ids: Vec<String> = crate::widgets::take_registry(&ctx).into_iter().map(|(id, _)| id).collect();
|
||||
assert!(ids.contains(&"effectStar:0".to_string()), "{ids:?}");
|
||||
assert!(!ids.contains(&"effectStar:29".to_string()), "{ids:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! The star rating: clicking sets the rating, clicking the current one clears it, a read-only or
|
||||
//! disabled row ignores clicks, the arrow keys step it, and screen readers hear every star.
|
||||
//! The star rating: clicking sets the rating, clicking the current one clears it, the row is one
|
||||
//! Tab stop, the arrow keys step it, and screen readers hear every star.
|
||||
|
||||
use egui::{Pos2, Rect, vec2};
|
||||
|
||||
@@ -134,3 +134,32 @@ fn arrows_keep_stepping_between_other_focusable_widgets() {
|
||||
let seen = step_with_neighbours(0, &[Tab, Tab, ArrowRight, ArrowRight, ArrowRight, ArrowRight, ArrowRight, ArrowLeft]);
|
||||
assert_eq!(seen.last(), Some(&4), "{seen:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rating_row_is_one_tab_stop() {
|
||||
use egui::Key::Tab;
|
||||
// left neighbour, one stop for the five stars, right neighbour: the third Tab is already past the row
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut focused = Vec::new();
|
||||
for frame in 0..5 {
|
||||
let events = if frame >= 1 {
|
||||
vec![egui::Event::Key { key: Tab, physical_key: Some(Tab), pressed: true, repeat: false, modifiers: Default::default() }]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(600.0, 200.0))), events, ..Default::default() };
|
||||
let mut right = None;
|
||||
let mut out = ctx.run_ui(raw, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let _ = ui.button("left neighbour");
|
||||
let _ = row(3).show(ui);
|
||||
right = Some(ui.button("right neighbour").id);
|
||||
});
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
focused.push(right.is_some_and(|id| ctx.memory(|m| m.has_focus(id))));
|
||||
}
|
||||
// frames 1 to 3 press Tab: left neighbour, the row, right neighbour
|
||||
assert_eq!(focused, vec![false, false, false, true, false], "{focused:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user