Hatch: pick the pattern from swatches instead of typing its name (fixes #669) (#679)

Squash of PR #679 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
aldozarandClaude Opus 5.5 authored and GitHub committed 2026-10-12 03:21:49 -04:00
1 parent c2f8782cc2
commit 0675648de8
19 files changed
+324 -15

No files matched your search

+55 -6
View File
@@ -1,6 +1,7 @@
//! HATCH, GRADIENT, BOUNDARY, REGION: boundaries from pick points or selected closed objects.
mod patfile;
pub use patfile::{PatternChoice, pattern_choices};
pub(super) use patfile::{lines_for, run_patterns};
use cadcraft_color::Color;
@@ -10,7 +11,7 @@ use serde_json::{Value, json};
use super::helpers::lwpoly;
use super::*;
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
use crate::{Accept, EngineError, Input, Interactive, Picker, Prompt, Result, Session, Step};
mod assoc;
mod edit;
@@ -582,7 +583,11 @@ impl Interactive for HatchM {
fn prompt(&self, s: &Session) -> Prompt {
// The sub-prompts offer the current value: Enter keeps it.
match self.asking {
Some(HatchAsk::Pattern) => return Prompt::new("Enter a pattern name", Accept::TEXT).default(self.settings.pattern.clone()),
Some(HatchAsk::Pattern) => {
return Prompt::new("Enter a pattern name", Accept::TEXT)
.default(self.settings.pattern.clone())
.picker(Picker::HatchPattern(self.settings.pattern.clone()));
}
Some(HatchAsk::Scale) => {
return Prompt::new("Specify a scale for the hatch pattern", Accept::NUMBER).default(format!("{:.4}", self.settings.scale));
}
@@ -596,8 +601,10 @@ impl Interactive for HatchM {
Some(HatchAsk::Origin) => return Prompt::new("Specify the pattern origin", Accept::POINT).kw(&ORIGIN_KW).default("Use current"),
None => {}
}
// The pattern picker stays offered while boundaries are picked: a choice sets the pattern.
let picker = |p: Prompt| if self.gradient { p } else { p.picker(Picker::HatchPattern(self.settings.pattern.clone())) };
if self.selecting {
return Prompt::new("Select objects", Accept::SELECT).kw(&[
return picker(Prompt::new("Select objects", Accept::SELECT).kw(&[
"picK internal point",
"Undo",
"seTtings",
@@ -605,9 +612,9 @@ impl Interactive for HatchM {
"Angle",
"Origin",
"sEparate",
]);
]));
}
Prompt::new("Pick internal point", Accept::POINT).kw(&[
picker(Prompt::new("Pick internal point", Accept::POINT).kw(&[
"Select objects",
"Undo",
"seTtings",
@@ -616,7 +623,7 @@ impl Interactive for HatchM {
"Angle",
"Origin",
"sEparate",
])
]))
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
if let Some(a) = self.asking.take() {
@@ -670,6 +677,11 @@ impl Interactive for HatchM {
}
Ok(Step::Continue)
}
// A pattern chosen in the picker while boundaries are picked.
Input::Text(t) if !self.gradient => {
self.answer(s, HatchAsk::Pattern, &t)?;
Ok(Step::Continue)
}
// Enter ends boundary picking in either mode and creates the hatch.
Input::Enter => self.finish(s),
_ => Ok(Step::Continue),
@@ -738,6 +750,43 @@ mod tests {
assert!(hatch.gradient.is_none());
}
#[test]
fn the_pattern_picker_is_offered_and_its_choice_sets_the_pattern() {
let mut s = Session::new();
s.execute("rectang", &json!({ "p1": [0, 0], "p2": [10, 10] })).unwrap();
s.start("hatch").unwrap();
let picker = s.current_prompt().and_then(|p| p.picker);
assert_eq!(picker, Some(Picker::HatchPattern(HatchSettings::from(&s, &Value::Null, false).pattern)));
// A choice made in the picker while boundaries are picked.
s.input(Input::Text("AR-CONC".into())).unwrap();
assert_eq!(s.current_prompt().and_then(|p| p.picker), Some(Picker::HatchPattern("AR-CONC".into())));
// And at the pattern prompt.
s.input(Input::Keyword("Pattern".into())).unwrap();
assert!(s.current_prompt().unwrap().picker.is_some());
s.input(Input::Text("BRICK".into())).unwrap();
s.input(Input::Point(Vec2::new(5.0, 5.0))).unwrap();
s.input(Input::Enter).unwrap();
assert_eq!(only_hatch(&s).1.pattern, "BRICK");
// An unknown name keeps the pattern.
s.start("hatch").unwrap();
s.input(Input::Text("NO-SUCH".into())).unwrap();
assert_eq!(s.current_prompt().and_then(|p| p.picker), Some(Picker::HatchPattern("BRICK".into())));
// GRADIENT offers no pattern picker.
s.cancel();
s.start("gradient").unwrap();
assert!(s.current_prompt().unwrap().picker.is_none());
}
#[test]
fn pattern_choices_list_the_library_with_its_lines() {
let all = pattern_choices();
assert_eq!(all.first().map(|c| c.name.as_str()), Some("SOLID"));
assert!(all[0].lines.is_empty());
let ansi31 = all.iter().find(|c| c.name == "ANSI31").unwrap();
assert_eq!(ansi31.lines.len(), 1);
assert!(!ansi31.description.is_empty());
}
#[test]
fn repeated_picks_and_handles_in_one_region_do_not_produce_duplicate_loops() {
// One internal point
+5 -2
View File
@@ -10,7 +10,7 @@ use super::assoc::{detach, same_shape};
use super::{ORIGIN_KW, OriginMode, closed_loop, island_style};
use crate::cmd::helpers::lwpoly;
use crate::cmd::{bad, bool_or, point_param, str_param, targets};
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
use crate::{Accept, Input, Interactive, Picker, Prompt, Result, Session, Step};
/// Upper bound on hatches edited by one call.
const MAX_EDITED: usize = 10_000;
@@ -328,7 +328,10 @@ impl Interactive for HatchEditM {
};
Prompt::new("Island detection style", Accept::TEXT).kw(&["Ignore", "Outer", "Normal"]).default(cur)
}
Ask::Pattern => Prompt::new("Enter a pattern name", Accept::TEXT).default(h.map(|h| h.pattern).unwrap_or_default()),
Ask::Pattern => {
let current = h.map(|h| h.pattern).unwrap_or_default();
Prompt::new("Enter a pattern name", Accept::TEXT).default(current.clone()).picker(Picker::HatchPattern(current))
}
Ask::Scale => Prompt::new("Specify a scale for the hatch pattern", Accept::NUMBER).default(format!("{:.4}", h.map_or(1.0, |h| h.scale))),
Ask::Angle => Prompt::new("Specify an angle for the hatch pattern", Accept::NUMBER)
.default(format!("{:.0}", h.map_or(0.0, |h| h.angle.to_degrees()))),
+19
View File
@@ -44,6 +44,25 @@ pub(crate) fn names() -> Vec<String> {
v
}
/// A pattern offered for new hatches: a library pattern or one a `.pat` file loaded.
#[derive(Clone, Debug, PartialEq)]
pub struct PatternChoice {
pub name: String,
pub description: String,
/// The line families at unit scale (none for SOLID).
pub lines: Vec<PatternLine>,
}
/// Every pattern offered for new hatches, in the order of [`names`], for a pattern picker.
pub fn pattern_choices() -> Vec<PatternChoice> {
let mut v: Vec<PatternChoice> = standard_patterns()
.into_iter()
.map(|p| PatternChoice { name: p.name.to_string(), description: p.description.to_string(), lines: p.lines })
.collect();
v.extend(store().iter().map(|p| PatternChoice { name: p.name.clone(), description: p.description.clone(), lines: p.lines.clone() }));
v
}
/// Add the patterns of a `.pat` file's text (a pattern loaded before under the same name is
/// replaced). Returns the names added and the file's problems.
pub(crate) fn load(text: &str) -> (Vec<String>, Vec<String>) {
+2 -1
View File
@@ -37,8 +37,9 @@ use serde_json::Value;
pub use cadcraft_doc as doc;
pub use cadcraft_geom as geom;
pub use cadcraft_render as render;
pub use cmd::hatch::{PatternChoice, pattern_choices};
pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command};
pub use prompt::{Accept, Drag, Input, Interactive, Prompt, Step};
pub use prompt::{Accept, Drag, Input, Interactive, Picker, Prompt, Step};
#[derive(Debug, thiserror::Error)]
pub enum EngineError {
+18 -1
View File
@@ -45,17 +45,34 @@ pub struct Prompt {
/// index): Dynamic Input shows its dimensions ([`crate::grip_dims`]).
#[serde(skip_serializing_if = "Option::is_none")]
pub grip: Option<(Handle, usize)>,
/// A visual chooser the UI can show for this prompt; a choice made in it is entered as
/// [`Input::Text`].
#[serde(skip_serializing_if = "Option::is_none")]
pub picker: Option<Picker>,
}
/// A visual chooser for a prompt ([`Prompt::picker`]).
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Picker {
/// A hatch pattern by name (see [`crate::pattern_choices`]); holds the current pattern.
HatchPattern(String),
}
impl Prompt {
pub fn new(message: impl Into<String>, accept: Accept) -> Self {
Prompt { message: message.into(), keywords: Vec::new(), default: None, accept, base: None, deferred: false, grip: None }
Prompt { message: message.into(), keywords: Vec::new(), default: None, accept, base: None, deferred: false, grip: None, picker: None }
}
/// The grip being stretched (the `grip` field).
pub fn grip(mut self, g: (Handle, usize)) -> Self {
self.grip = Some(g);
self
}
/// The visual chooser the UI can show (the `picker` field).
pub fn picker(mut self, p: Picker) -> Self {
self.picker = Some(p);
self
}
pub fn kw(mut self, k: &[&str]) -> Self {
self.keywords = k.iter().map(|s| s.to_string()).collect();
self
+35
View File
@@ -349,6 +349,41 @@ pub fn pattern_lines(d: &Drawing, h: &Hatch, loops: &[Vec<Vec2>], px: f64, view:
out
}
/// Repeats of a pattern across a swatch ([`pattern_swatch`]).
const SWATCH_REPEATS: f64 = 6.0;
/// Most runs a swatch holds.
const MAX_SWATCH_RUNS: usize = 4000;
/// A preview of a pattern for a pattern picker: the runs of its line families `lines` inside the
/// `side` × `side` square with a corner at the origin, at a scale that shows a few repeats of the
/// pattern (each family's line spacing or dash repeat, whichever is longer). Empty for a pattern
/// with no lines (SOLID).
pub fn pattern_swatch(lines: &[PatternLine], side: f64) -> Vec<Vec<Vec2>> {
let repeat =
lines.iter().map(|l| l.delta.1.abs().max(l.dashes.iter().map(|d| d.abs()).sum::<f64>())).filter(|r| r.is_finite()).fold(0.0, f64::max);
if !side.is_finite() || side <= 0.0 || repeat < 1e-9 {
return Vec::new();
}
let h = Hatch {
pattern: String::new(),
solid: false,
loops: Vec::new(),
scale: side / (SWATCH_REPEATS * repeat),
angle: 0.0,
associative: false,
style: 0,
elevation: 0.0,
gradient: None,
origin: Vec2::ZERO,
background: None,
pattern_lines: lines.to_vec(),
};
let square = vec![Vec2::new(0.0, 0.0), Vec2::new(side, 0.0), Vec2::new(side, side), Vec2::new(0.0, side)];
let mut runs = pattern_lines(&Drawing::default(), &h, &[square], 0.0, None);
runs.truncate(MAX_SWATCH_RUNS);
runs
}
/// One pattern line family, scaled and placed: parallel lines `spacing` apart through `origin`
/// at angle `ang`, each shifted along itself by `delta`'s along-line part from the last.
struct Family<'a> {
+1
View File
@@ -31,6 +31,7 @@ use pen::Pen;
pub use dim::{ArrowGeom, Arrowhead, DimGeometry, DimText, LineRole, arrowhead, dimension_geometry, dimension_geometry_with, format_linear_value};
pub use fill::triangulate_evenodd;
pub use hatch::pattern_swatch;
pub use paper::{PAPER_SIZES, PaperSize, Sheet, paper_size, sheet};
pub use tolerance::{gdt_points, tolerance_in};
+17
View File
@@ -165,6 +165,23 @@ fn pattern_lines_from_a_file_are_drawn_for_unknown_names() {
assert!(((p[1] - p[0]).angle().to_degrees().rem_euclid(180.0) - 45.0).abs() < 1e-6);
}
#[test]
fn pattern_swatches_stay_in_their_square_and_show_a_few_repeats() {
for p in cadcraft_doc::library::standard_patterns() {
let runs = cadcraft_render::pattern_swatch(&p.lines, 1.0);
if p.lines.is_empty() {
assert!(runs.is_empty(), "{}", p.name);
continue;
}
assert!(runs.len() >= 3, "{} has {} runs", p.name, runs.len());
assert!(runs.len() <= 4000, "{} has {} runs", p.name, runs.len());
for v in runs.iter().flatten() {
assert!((-1e-9..=1.0 + 1e-9).contains(&v.x) && (-1e-9..=1.0 + 1e-9).contains(&v.y), "{} at {v:?}", p.name);
}
}
assert!(cadcraft_render::pattern_swatch(&[], 1.0).is_empty());
}
#[test]
fn brick_joints_are_drawn_once_half_a_brick_apart_course_to_course() {
let l = draw(hatch("BRICK", vec![square(0.0, 2.0)]), "0");
+2
View File
@@ -1206,3 +1206,5 @@ menu View Zobrazení
menu Draw Kreslení
menu Dimension Kótování
menu Modify Modifikace
Hatch Pattern Vzor šrafy
Search for pattern Hledat vzor
1 # CADCraft Czech interface catalog — original translations by @Losminek (AI-assisted).
1206 menu
1207 menu
1208 menu
1209
1210
+2
View File
@@ -1013,3 +1013,5 @@
Calibrate Kalibrieren
Move sideways to pan; push/pull or twist to zoom. Seitlich bewegen zum Verschieben; drücken, ziehen oder drehen zum Zoomen.
Left device button: zoom extents. Right device button: calibrate. Linke Gerätetaste: auf Grenzen zoomen. Rechte Gerätetaste: kalibrieren.
Hatch Pattern Schraffurmuster
Search for pattern Muster suchen
1 # CADCraft German interface catalog — original translations by the CADCraft contributors.
1013
1014
1015
1016
1017
+2
View File
@@ -1232,3 +1232,5 @@
Calibrate Calibrar
Move sideways to pan; push/pull or twist to zoom. Mueve el mando lateralmente para encuadrar; empuja, tira o gira para hacer zoom.
Left device button: zoom extents. Right device button: calibrate. Botón izquierdo del dispositivo: zoom a la extensión. Botón derecho: calibrar.
Hatch Pattern Patrón de sombreado
Search for pattern Buscar patrón
1 # CADCraft Spanish interface catalog — original translations.
1232
1233
1234
1235
1236
+2
View File
@@ -996,3 +996,5 @@
Import... Importer...
Export... Exporter...
Batch Publish... Publication par lots...
Hatch Pattern Motif de hachure
Search for pattern Rechercher un motif
1 # CADCraft French interface catalog — original translations by @pechnikov2026-star.
996
997
998
999
1000
+2
View File
@@ -995,3 +995,5 @@
Hide Others Ocultar outros
Accessibility Acessibilidade
Show System Cursor Exibir cursor do sistema
Hatch Pattern Padrão de hachura
Search for pattern Pesquisar padrão
1 # CADCraft Brazilian Portuguese (pt-BR) interface catalog.
995
996
997
998
999
+2
View File
@@ -996,3 +996,5 @@
Import... Импорт...
Export... Экспорт...
Batch Publish... Пакетная публикация...
Hatch Pattern Образец штриховки
Search for pattern Поиск образца
1 # CADCraft Russian interface catalog — original translations by @pechnikov2026-star.
996
997
998
999
1000
+1
View File
@@ -94,6 +94,7 @@ fn missing_marked_ui_literals(language: &str) -> BTreeSet<String> {
include_str!("../cmdline.rs"),
include_str!("../credits.rs"),
include_str!("../palettes/quickcalc.rs"),
include_str!("../palettes/patterns.rs"),
include_str!("../dialogs.rs"),
include_str!("../layers.rs"),
include_str!("../canvas/compare.rs"),
+2
View File
@@ -1230,4 +1230,6 @@
Drawings with no file yet are saved in the default version. 0 minutes turns automatic save off. Креслення, що ще не мають файлу, зберігаються в типовій версії. 0 хвилин вимикає автозбереження.
Time-sensitive right-click: a quick click is Enter, holding opens the menu Клік правою кнопкою з урахуванням часу: швидкий клік — Enter, утримання відкриває меню
Turned off, a right-click while a command runs always opens the shortcut menu. Якщо вимкнено, клік правою кнопкою під час команди завжди відкриває контекстне меню.
Hatch Pattern Зразок штрихування
Search for pattern Пошук зразка
Run Script... Виконати сценарій...
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
1230
1231
1232
1233
1234
1235
+1
View File
@@ -498,6 +498,7 @@ impl CadApp {
}
});
dialogs::show(self, ui.ctx());
palettes::patterns::prompt_picker(self, ui.ctx());
closing::prompt(self, ui.ctx());
autosave::show(self, ui.ctx());
autosave::gpu_notice(self, ui.ctx());
+5 -5
View File
@@ -9,6 +9,8 @@ use crate::CadApp;
use crate::icons::{self, Icon};
use crate::theme::Tokens;
/// The hatch pattern picker.
pub mod patterns;
/// The QuickCalc palette.
pub mod quickcalc;
@@ -1014,11 +1016,9 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
EntityKind::Hatch(hh) => {
prop_row(ui, "Pattern", |ui| {
ui.menu_button(hh.pattern.clone(), |ui| {
for pat in cadcraft_doc::library::standard_patterns() {
if ui.button(pat.name).clicked() {
set = Some(json!({ "handles": ids, "pattern": pat.name }));
ui.close();
}
if let Some(name) = patterns::menu(ui, &hh.pattern) {
set = Some(json!({ "handles": ids, "pattern": name }));
ui.close();
}
});
});
+151
View File
@@ -0,0 +1,151 @@
//! The hatch pattern picker: a swatch of every pattern offered for new hatches, beside HATCH's
//! and HATCHEDIT's prompts and in the Properties palette, so a pattern is chosen by its look
//! instead of by typing its name.
use std::cell::RefCell;
use std::sync::Arc;
use cadcraft_engine::geom::Vec2;
use cadcraft_engine::{Input, PatternChoice, Picker, pattern_choices};
use egui::{Align2, Rect, Sense, Stroke, pos2, vec2};
use crate::CadApp;
use crate::theme::Tokens;
/// A swatch's side, in points.
const CELL: f32 = 56.0;
/// Room under a swatch for the pattern's name.
const LABEL_H: f32 = 16.0;
/// Space between swatches.
const GAP: f32 = 6.0;
/// Swatches per row in the Properties palette's menu.
const MENU_COLUMNS: f32 = 6.0;
/// A pattern as the picker draws it: its runs in the unit square (y up), or a solid fill.
struct Swatch {
name: String,
description: String,
runs: Vec<Vec<Vec2>>,
solid: bool,
}
/// The patterns' names and line family counts, and their swatches.
type Swatches = (Vec<(String, usize)>, Arc<Vec<Swatch>>);
thread_local! {
static SWATCHES: RefCell<Swatches> = RefCell::new((Vec::new(), Arc::new(Vec::new())));
}
/// The swatches of the patterns offered now, made again only when the list changes (a `.pat`
/// file loaded more patterns).
fn swatches() -> Arc<Vec<Swatch>> {
let choices = pattern_choices();
let key: Vec<(String, usize)> = choices.iter().map(|c| (c.name.clone(), c.lines.len())).collect();
SWATCHES.with(|cell| {
let mut cache = cell.borrow_mut();
if cache.0 != key {
*cache = (key, Arc::new(choices.into_iter().map(swatch).collect()));
}
cache.1.clone()
})
}
fn swatch(c: PatternChoice) -> Swatch {
let solid = c.lines.is_empty();
Swatch { runs: cadcraft_engine::render::pattern_swatch(&c.lines, 1.0), name: c.name, description: c.description, solid }
}
/// Draw one swatch in `r` (the square and the name under it).
fn paint(p: &egui::Painter, r: Rect, s: &Swatch, selected: bool, hovered: bool) {
let t = Tokens::get();
let square = Rect::from_min_size(r.min, vec2(CELL, CELL));
p.rect_filled(square, 3.0, t.field);
let inner = square.shrink(3.0);
if s.solid {
p.rect_filled(inner, 1.0, t.text);
} else {
let at = |v: &Vec2| pos2(inner.min.x + v.x as f32 * inner.width(), inner.max.y - v.y as f32 * inner.height());
let stroke = Stroke::new(1.0, t.text);
for run in &s.runs {
match run.as_slice() {
[] => {}
[dot] => {
p.circle_filled(at(dot), 0.7, t.text);
}
pts => {
p.add(egui::Shape::line(pts.iter().map(at).collect(), stroke));
}
}
}
}
let border = match (selected, hovered) {
(true, _) => Stroke::new(2.0, t.accent),
(false, true) => Stroke::new(1.0, t.accent),
_ => Stroke::new(1.0, t.border),
};
p.rect_stroke(square, 3.0, border, egui::StrokeKind::Inside);
let label = Rect::from_min_max(pos2(r.min.x, square.max.y), r.max);
let color = if selected { t.accent } else { t.text_dim };
p.with_clip_rect(label).text(label.center_top() + vec2(0.0, 2.0), Align2::CENTER_TOP, &s.name, crate::theme::small(), color);
}
/// The swatches whose name or description holds `filter`, `current` marked; returns the
/// pattern clicked.
pub fn grid(ui: &mut egui::Ui, current: &str, filter: &str) -> Option<String> {
let filter = filter.trim().to_lowercase();
let mut chosen = None;
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing = vec2(GAP, GAP);
let shown = swatches();
for s in
shown.iter().filter(|s| filter.is_empty() || s.name.to_lowercase().contains(&filter) || s.description.to_lowercase().contains(&filter))
{
let (r, resp) = ui.allocate_exact_size(vec2(CELL, CELL + LABEL_H), Sense::click());
if ui.is_rect_visible(r) {
paint(ui.painter(), r, s, s.name.eq_ignore_ascii_case(current), resp.hovered());
}
let tip = if s.description.is_empty() { s.name.clone() } else { format!("{}\n{}", s.name, s.description) };
if resp.on_hover_text(tip).clicked() {
chosen = Some(s.name.clone());
}
}
});
chosen
}
/// The Properties palette's pattern menu: the swatch grid, scrolling; returns the pattern
/// clicked.
pub fn menu(ui: &mut egui::Ui, current: &str) -> Option<String> {
ui.set_width(MENU_COLUMNS * (CELL + GAP));
egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| grid(ui, current, "")).inner
}
/// The picker window while the prompt offers one (HATCH's boundary and pattern prompts,
/// HATCHEDIT's pattern prompt): a click enters the pattern's name at the prompt.
pub fn prompt_picker(app: &mut CadApp, ctx: &egui::Context) {
let Some(Picker::HatchPattern(current)) = app.session.current_prompt().and_then(|p| p.picker) else { return };
let filter_id = egui::Id::new("hatch_pattern_filter");
let mut filter: String = ctx.data_mut(|d| d.get_temp(filter_id)).unwrap_or_default();
let mut chosen = None;
let area = ctx.content_rect();
egui::Window::new(crate::tl!("Hatch Pattern"))
.id(egui::Id::new("Hatch Pattern"))
.collapsible(true)
.resizable(true)
.default_size(vec2(5.0 * (CELL + GAP) + 12.0, 5.0 * (CELL + LABEL_H + GAP) + 40.0))
.default_pos(pos2(area.max.x - 5.0 * (CELL + GAP) - 330.0, area.min.y + 120.0))
.show(ctx, |ui| {
ui.add(egui::TextEdit::singleline(&mut filter).hint_text(crate::tl!("Search for pattern")).desired_width(f32::INFINITY));
ui.add_space(4.0);
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
chosen = grid(ui, &current, &filter);
});
});
ctx.data_mut(|d| d.insert_temp(filter_id, filter));
if let Some(name) = chosen {
app.cmd.buffer.clear();
if let Err(e) = app.session.input(Input::Text(name)) {
app.session.echo(e.to_string());
}
}
}