mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-27 11:37:13 -06:00
LR-BEHAV-TEXTFIELD: the search field is a shared text field
Search Photos gets the Cut / Copy / Paste / Select All menu, and Esc gives back the search from before the edit (and filters by it again). The field gains the builder options it needs (no frame, font, colour, alignment, margin) and `text_field::id` for ⌘F to focus it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
94ee52088d
commit
4efd92b38f
@@ -678,6 +678,41 @@ mod tests {
|
||||
assert!(egui::Popup::is_any_open(&h.view.ctx), "its context menu");
|
||||
}
|
||||
|
||||
/// The search field is a shared text field: right-click ▸ Paste puts the clipboard's text in
|
||||
/// and filters by it.
|
||||
#[test]
|
||||
fn the_search_field_pastes_from_its_menu() {
|
||||
let mut h = demo([1200.0, 760.0]);
|
||||
let t = Duration::from_secs(10);
|
||||
h.settle(SETTLE);
|
||||
h.view.clipboard = "wedding".into();
|
||||
for (id, button) in [("field:search", "right"), ("field:search:paste", "left")] {
|
||||
let r = h.request("ui.clickWidget", json!({"id": id, "button": button}), t);
|
||||
assert_eq!(r["ok"], true, "{id}: {r}");
|
||||
h.settle(SETTLE);
|
||||
}
|
||||
assert_eq!(h.app.ui.search, "wedding");
|
||||
assert_eq!(h.app.session.filter.text, "wedding", "and the library is filtered by it");
|
||||
}
|
||||
|
||||
/// Esc in the search field gives back the search from before the edit, and the filter with it.
|
||||
#[test]
|
||||
fn escape_in_the_search_field_gives_back_the_search() {
|
||||
let mut h = demo([1200.0, 760.0]);
|
||||
let t = Duration::from_secs(10);
|
||||
h.settle(SETTLE);
|
||||
for (method, params) in [("ui.clickWidget", json!({"id": "field:search"})), ("ui.text", json!({"text": "travel"}))] {
|
||||
let r = h.request(method, params, t);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
}
|
||||
assert_eq!(h.app.session.filter.text, "travel", "filtered while typing");
|
||||
let r = h.request("ui.key", json!({"key": "Escape"}), t);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
assert_eq!((h.app.ui.search.as_str(), h.app.session.filter.text.as_str()), ("", ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn demo_grid_snapshot_has_ui_pixels() {
|
||||
let t0 = Instant::now();
|
||||
|
||||
@@ -79,7 +79,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
// search field (centred on the window)
|
||||
let sr = Rect::from_center_size(pos2(full.center().x, full.center().y), vec2(sw, 28.0));
|
||||
let id = egui::Id::new("search-field");
|
||||
let id = crate::text_field::id("field:search");
|
||||
if std::mem::take(&mut app.ui.focus_search) {
|
||||
ui.memory_mut(|m| m.request_focus(id));
|
||||
}
|
||||
@@ -91,7 +91,6 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
Stroke::new(1.0, if focused { t.accent } else { t.field_border }),
|
||||
StrokeKind::Inside,
|
||||
);
|
||||
register(ui.ctx(), "field:search", sr);
|
||||
let mut child =
|
||||
ui.new_child(egui::UiBuilder::new().max_rect(sr.shrink2(vec2(10.0, 4.0))).layout(egui::Layout::left_to_right(egui::Align::Center)));
|
||||
let empty = app.ui.search.is_empty();
|
||||
@@ -102,14 +101,13 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
paint(child.painter(), Rect::from_min_size(pos2(x0, sr.center().y - 8.0), vec2(16.0, 16.0)), Icon::Search, t.text_dim);
|
||||
child.painter().galley(pos2(x0 + 24.0, sr.center().y - g.size().y / 2.0), g, t.text_dim);
|
||||
}
|
||||
let resp = child.add(
|
||||
egui::TextEdit::singleline(&mut app.ui.search)
|
||||
.id(id)
|
||||
.frame(egui::Frame::NONE)
|
||||
.desired_width(sr.width() - 20.0)
|
||||
.font(t.font(13.5))
|
||||
.text_color(t.text),
|
||||
);
|
||||
let resp = crate::text_field::TextField::singleline("field:search", &mut app.ui.search)
|
||||
.frame(false)
|
||||
.width(sr.width() - 20.0)
|
||||
.font(t.font(13.5))
|
||||
.text_color(t.text)
|
||||
.show(&mut child)
|
||||
.response;
|
||||
if resp.changed() {
|
||||
let q = app.ui.search.clone();
|
||||
let _ = app.run("library.filter", json!({"text": q}));
|
||||
|
||||
@@ -47,12 +47,34 @@ pub struct TextField<'a> {
|
||||
hint: Option<String>,
|
||||
width: Option<f32>,
|
||||
select_on_focus: bool,
|
||||
frame: bool,
|
||||
font: Option<egui::FontId>,
|
||||
color: Option<egui::Color32>,
|
||||
align: Option<egui::Align>,
|
||||
margin: Option<egui::Margin>,
|
||||
}
|
||||
|
||||
/// The egui id of the field registered as `widget`, to give it the focus (⌘F to search).
|
||||
pub fn id(widget: &str) -> egui::Id {
|
||||
egui::Id::new("lc-text-field").with(widget)
|
||||
}
|
||||
|
||||
impl<'a> TextField<'a> {
|
||||
/// A one-line field (Return ends the edit) registered as `widget`.
|
||||
pub fn singleline(widget: &'a str, text: &'a mut String) -> Self {
|
||||
TextField { widget, text, multiline: false, hint: None, width: None, select_on_focus: false }
|
||||
TextField {
|
||||
widget,
|
||||
text,
|
||||
multiline: false,
|
||||
hint: None,
|
||||
width: None,
|
||||
select_on_focus: false,
|
||||
frame: true,
|
||||
font: None,
|
||||
color: None,
|
||||
align: None,
|
||||
margin: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A multi-line field: Return starts a new line; leaving the field ends the edit.
|
||||
@@ -71,6 +93,32 @@ impl<'a> TextField<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Draw the field's own box (on by default); off where the caller draws one around it.
|
||||
pub fn frame(mut self, on: bool) -> Self {
|
||||
self.frame = on;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn font(mut self, font: egui::FontId) -> Self {
|
||||
self.font = Some(font);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text_color(mut self, color: egui::Color32) -> Self {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn horizontal_align(mut self, align: egui::Align) -> Self {
|
||||
self.align = Some(align);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn margin(mut self, margin: egui::Margin) -> Self {
|
||||
self.margin = Some(margin);
|
||||
self
|
||||
}
|
||||
|
||||
/// Select all the text when the field takes focus, so typing replaces it (names, numbers).
|
||||
pub fn select_on_focus(mut self, on: bool) -> Self {
|
||||
self.select_on_focus = on;
|
||||
@@ -78,7 +126,7 @@ impl<'a> TextField<'a> {
|
||||
}
|
||||
|
||||
pub fn show(self, ui: &mut Ui) -> TextFieldResponse {
|
||||
let id = egui::Id::new("lc-text-field").with(self.widget);
|
||||
let id = id(self.widget);
|
||||
let mut memo: Memo = ui.data_mut(|d| d.get_temp(id.with("memo"))).unwrap_or_default();
|
||||
// a choice from the menu last frame: the field takes the focus back and does it, with
|
||||
// egui's own handling of the clipboard events (a password is never copied)
|
||||
@@ -100,6 +148,21 @@ impl<'a> TextField<'a> {
|
||||
if let Some(w) = self.width {
|
||||
edit = edit.desired_width(w);
|
||||
}
|
||||
if !self.frame {
|
||||
edit = edit.frame(egui::Frame::NONE);
|
||||
}
|
||||
if let Some(font) = self.font {
|
||||
edit = edit.font(font);
|
||||
}
|
||||
if let Some(color) = self.color {
|
||||
edit = edit.text_color(color);
|
||||
}
|
||||
if let Some(align) = self.align {
|
||||
edit = edit.horizontal_align(align);
|
||||
}
|
||||
if let Some(margin) = self.margin {
|
||||
edit = edit.margin(margin);
|
||||
}
|
||||
let (escape, enter) = ui.input(|i| (i.key_pressed(egui::Key::Escape), i.key_pressed(egui::Key::Enter)));
|
||||
// a right-click places the cursor as a left one does: the selection the menu acts on
|
||||
// would be gone
|
||||
|
||||
Reference in New Issue
Block a user