| 246 |
246 |
|
outcome
|
| 247 |
247 |
|
}
|
| 248 |
248 |
|
|
| 249 |
|
- |
/// Single-field name modal: title, optional hint, label, text input,
|
|
249 |
+ |
/// Everything a [`name_modal`] draws except the answer itself.
|
|
250 |
+ |
///
|
|
251 |
+ |
/// A struct rather than more positional parameters, for the reason
|
|
252 |
+ |
/// [`ConfirmSpec`] is one: the modal already took seven, and the two message
|
|
253 |
+ |
/// slots it now separates (`lead_in` and `hint`) are both `Option<&str>` and
|
|
254 |
+ |
/// would be indistinguishable at a call site.
|
|
255 |
+ |
///
|
|
256 |
+ |
/// `lead_in` and `hint` are that separation. A lead-in is about the modal as a
|
|
257 |
+ |
/// whole, so it stays above the field ("A vault is a separate sample
|
|
258 |
+ |
/// collection..."); a hint is standing help about the answer, so it belongs to
|
|
259 |
+ |
/// the field and renders under the input ("Folder names cannot contain /").
|
|
260 |
+ |
/// Same line the export screen draws between a field's message and its footer.
|
|
261 |
+ |
pub struct NameModalSpec<'a> {
|
|
262 |
+ |
/// Window title.
|
|
263 |
+ |
pub title: &'a str,
|
|
264 |
+ |
/// Standing help about the modal, drawn above the field.
|
|
265 |
+ |
pub lead_in: Option<&'a str>,
|
|
266 |
+ |
/// The field's label. No trailing colon: `field()` draws the label.
|
|
267 |
+ |
pub label: &'a str,
|
|
268 |
+ |
/// Standing help about the answer, drawn under the input.
|
|
269 |
+ |
pub hint: Option<&'a str>,
|
|
270 |
+ |
/// Ghost text shown while the input is empty.
|
|
271 |
+ |
pub placeholder: Option<&'a str>,
|
|
272 |
+ |
/// Label on the submit button.
|
|
273 |
+ |
pub submit_label: &'a str,
|
|
274 |
+ |
/// What is wrong with the answer now, drawn under the input.
|
|
275 |
+ |
pub error: Option<&'a str>,
|
|
276 |
+ |
}
|
|
277 |
+ |
|
|
278 |
+ |
/// Single-field name modal: title, optional lead-in, one described text field,
|
| 250 |
279 |
|
/// submit/cancel. Enter in the field submits.
|
| 251 |
280 |
|
///
|
|
281 |
+ |
/// The field is described rather than hand-drawn, which is what moves the label,
|
|
282 |
+ |
/// the hint and the error onto the question they are about and paints the input
|
|
283 |
+ |
/// the same well every other text input in the app has. It is deliberately not
|
|
284 |
+ |
/// `required`: an empty submit closes the modal as a no-op (see
|
|
285 |
+ |
/// `handle_name_modal_outcome`), so the marker would claim a refusal that never
|
|
286 |
+ |
/// happens. Same call the license-key field made.
|
|
287 |
+ |
///
|
| 252 |
288 |
|
/// Autofocus: the text field grabs focus on first open (detected as "input is
|
| 253 |
289 |
|
/// empty and nothing in the app currently has focus"). After the user clicks
|
| 254 |
290 |
|
/// any widget the autofocus stops firing, so Cancel/Submit clicks aren't
|
| 255 |
291 |
|
/// stolen back by the input.
|
| 256 |
292 |
|
pub fn name_modal(
|
| 257 |
293 |
|
ctx: &egui::Context,
|
| 258 |
|
- |
title: &str,
|
| 259 |
|
- |
hint: Option<&str>,
|
| 260 |
|
- |
label: &str,
|
|
294 |
+ |
spec: &NameModalSpec<'_>,
|
| 261 |
295 |
|
input: &mut String,
|
| 262 |
|
- |
submit_label: &str,
|
| 263 |
|
- |
error: Option<&str>,
|
| 264 |
296 |
|
) -> NameModalOutcome {
|
| 265 |
297 |
|
let mut outcome = NameModalOutcome::None;
|
| 266 |
|
- |
modal_window(ctx, title, false, None, |ui| {
|
| 267 |
|
- |
if let Some(h) = hint {
|
| 268 |
|
- |
ui.label(egui::RichText::new(h).small().color(theme::content_muted()));
|
|
298 |
+ |
modal_window(ctx, spec.title, false, None, |ui| {
|
|
299 |
+ |
if let Some(lead_in) = spec.lead_in {
|
|
300 |
+ |
ui.label(
|
|
301 |
+ |
egui::RichText::new(lead_in)
|
|
302 |
+ |
.small()
|
|
303 |
+ |
.color(theme::content_muted()),
|
|
304 |
+ |
);
|
| 269 |
305 |
|
ui.add_space(theme::space::bound());
|
| 270 |
306 |
|
}
|
| 271 |
|
- |
ui.label(label);
|
| 272 |
|
- |
let resp = ui.text_edit_singleline(input);
|
| 273 |
|
- |
if input.is_empty() && ui.memory(|m| m.focused().is_none()) {
|
| 274 |
|
- |
resp.request_focus();
|
| 275 |
|
- |
}
|
| 276 |
|
- |
// C-3: inline error below the input. Re-focus the input when an error
|
| 277 |
|
- |
// is surfaced so the user can edit and retry without re-clicking.
|
| 278 |
|
- |
if let Some(err) = error {
|
| 279 |
|
- |
ui.add_space(theme::space::hair());
|
| 280 |
|
- |
ui.label(egui::RichText::new(err).small().color(theme::danger()));
|
| 281 |
|
- |
if !resp.has_focus() {
|
|
307 |
+ |
let name_field = makeover_layout::Field {
|
|
308 |
+ |
hint: spec.hint,
|
|
309 |
+ |
placeholder: spec.placeholder,
|
|
310 |
+ |
error: spec.error,
|
|
311 |
+ |
..makeover_layout::Field::new(makeover_layout::FieldKind::Text, "name", spec.label)
|
|
312 |
+ |
};
|
|
313 |
+ |
let resp = field(
|
|
314 |
+ |
ui,
|
|
315 |
+ |
&name_field,
|
|
316 |
+ |
makeover_immediate::Filling::Text(&mut *input),
|
|
317 |
+ |
None,
|
|
318 |
+ |
);
|
|
319 |
+ |
if let Some(resp) = resp {
|
|
320 |
+ |
if input.is_empty() && ui.memory(|m| m.focused().is_none()) {
|
| 282 |
321 |
|
resp.request_focus();
|
| 283 |
322 |
|
}
|
| 284 |
|
- |
}
|
| 285 |
|
- |
if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
| 286 |
|
- |
outcome = NameModalOutcome::Submitted(input.trim().to_string());
|
|
323 |
+ |
// C-3: re-focus the input when an error is surfaced so the user can
|
|
324 |
+ |
// edit and retry without re-clicking. The message itself is the
|
|
325 |
+ |
// field's now, so nothing here draws it.
|
|
326 |
+ |
if spec.error.is_some() && !resp.has_focus() {
|
|
327 |
+ |
resp.request_focus();
|
|
328 |
+ |
}
|
|
329 |
+ |
if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
|
330 |
+ |
outcome = NameModalOutcome::Submitted(input.trim().to_string());
|
|
331 |
+ |
}
|
| 287 |
332 |
|
}
|
| 288 |
333 |
|
ui.add_space(theme::space::peer());
|
| 289 |
334 |
|
ui.horizontal(|ui| {
|
| 290 |
335 |
|
if secondary_button(ui, "Cancel").clicked() {
|
| 291 |
336 |
|
outcome = NameModalOutcome::Cancelled;
|
| 292 |
337 |
|
}
|
| 293 |
|
- |
if secondary_button(ui, submit_label).clicked() {
|
|
338 |
+ |
if secondary_button(ui, spec.submit_label).clicked() {
|
| 294 |
339 |
|
outcome = NameModalOutcome::Submitted(input.trim().to_string());
|
| 295 |
340 |
|
}
|
| 296 |
341 |
|
});
|