| 157 |
157 |
|
) -> ConfirmOutcome {
|
| 158 |
158 |
|
let mut outcome = ConfirmOutcome::None;
|
| 159 |
159 |
|
ui.horizontal(|ui| {
|
| 160 |
|
- |
if ui.button("Cancel").clicked() {
|
|
160 |
+ |
// Through the helpers rather than raw `ui.button`, so this row gets the
|
|
161 |
+ |
// control radius. Square is the default now and a dialog action row is
|
|
162 |
+ |
// the one place that is wrong: a push button is what a corner means.
|
|
163 |
+ |
if secondary_button(ui, "Cancel").clicked() {
|
| 161 |
164 |
|
outcome = ConfirmOutcome::Cancelled;
|
| 162 |
165 |
|
}
|
| 163 |
|
- |
let label = if danger {
|
| 164 |
|
- |
egui::RichText::new(confirm_label).color(theme::danger())
|
|
166 |
+ |
let confirmed = if danger {
|
|
167 |
+ |
danger_button_enabled(ui, confirm_label, can_confirm).clicked()
|
| 165 |
168 |
|
} else {
|
| 166 |
|
- |
egui::RichText::new(confirm_label)
|
| 167 |
|
- |
};
|
| 168 |
|
- |
if ui
|
| 169 |
|
- |
.add_enabled(can_confirm, egui::Button::new(label))
|
|
169 |
+ |
// Not `primary_button`: that one has no disabled variant, and
|
|
170 |
+ |
// adding one is a weight change to every modal, which is a
|
|
171 |
+ |
// separate decision from the shape.
|
|
172 |
+ |
ui.add_enabled(
|
|
173 |
+ |
can_confirm,
|
|
174 |
+ |
egui::Button::new(confirm_label).corner_radius(theme::radius_control()),
|
|
175 |
+ |
)
|
| 170 |
176 |
|
.clicked()
|
| 171 |
|
- |
{
|
|
177 |
+ |
};
|
|
178 |
+ |
if confirmed {
|
| 172 |
179 |
|
outcome = ConfirmOutcome::Confirmed;
|
| 173 |
180 |
|
}
|
| 174 |
181 |
|
});
|
| 378 |
385 |
|
.clicked()
|
| 379 |
386 |
|
}
|
| 380 |
387 |
|
|
| 381 |
|
- |
/// Mutually-exclusive segmented pill control.
|
|
388 |
+ |
/// Mutually-exclusive segmented control.
|
| 382 |
389 |
|
///
|
| 383 |
390 |
|
/// `options` is a list of `(value, label, tooltip)` triples. Returns
|
| 384 |
391 |
|
/// `Some(value)` if a non-current option was clicked, `None` otherwise.
|
| 385 |
392 |
|
/// Caller assigns the returned value to its state.
|
| 386 |
|
- |
pub fn toggle_pills<T: Clone + PartialEq>(
|
|
393 |
+ |
///
|
|
394 |
+ |
/// The segments abut and each carries its own bevel, so the control reads as
|
|
395 |
+ |
/// one object with one part pushed in. Two spaced pills read as two unrelated
|
|
396 |
+ |
/// buttons that happen to sit near each other, which is what this was before
|
|
397 |
+ |
/// and why nothing about it said "pick exactly one of these".
|
|
398 |
+ |
pub fn segmented_control<T: Clone + PartialEq>(
|
| 387 |
399 |
|
ui: &mut egui::Ui,
|
| 388 |
400 |
|
current: &T,
|
| 389 |
401 |
|
options: &[(T, &str, &str)],
|
| 390 |
402 |
|
) -> Option<T> {
|
| 391 |
403 |
|
let mut chosen = None;
|
| 392 |
404 |
|
ui.horizontal(|ui| {
|
|
405 |
+ |
// No gap between segments. The gap is the whole difference between a
|
|
406 |
+ |
// segmented control and a row of buttons.
|
|
407 |
+ |
ui.spacing_mut().item_spacing.x = 0.0;
|
|
408 |
+ |
|
| 393 |
409 |
|
for (value, label, tooltip) in options {
|
| 394 |
410 |
|
let is_active = value == current;
|
| 395 |
|
- |
if ui
|
| 396 |
|
- |
.selectable_label(is_active, *label)
|
| 397 |
|
- |
.on_hover_text(*tooltip)
|
| 398 |
|
- |
.clicked()
|
| 399 |
|
- |
&& !is_active
|
| 400 |
|
- |
{
|
|
411 |
+ |
let (fill, text) = if is_active {
|
|
412 |
+ |
(theme::surface_sunken(), theme::content())
|
|
413 |
+ |
} else {
|
|
414 |
+ |
(theme::surface_raised(), theme::content_secondary())
|
|
415 |
+ |
};
|
|
416 |
+ |
|
|
417 |
+ |
let resp = ui
|
|
418 |
+ |
.add(
|
|
419 |
+ |
egui::Button::new(egui::RichText::new(*label).color(text))
|
|
420 |
+ |
.corner_radius(theme::radius_container())
|
|
421 |
+ |
.fill(fill),
|
|
422 |
+ |
)
|
|
423 |
+ |
.on_hover_text(*tooltip);
|
|
424 |
+ |
|
|
425 |
+ |
// The selected segment is held in, and a segment under a held
|
|
426 |
+ |
// pointer shows the same thing. One rule, two reasons to be inset:
|
|
427 |
+ |
// this is what the light model buys over a colour swap.
|
|
428 |
+ |
let kind = if is_active || resp.is_pointer_button_down_on() {
|
|
429 |
+ |
theme::bevel::Bevel::Inset
|
|
430 |
+ |
} else {
|
|
431 |
+ |
theme::bevel::Bevel::Raised
|
|
432 |
+ |
};
|
|
433 |
+ |
theme::bevel::paint(ui.painter(), resp.rect, kind);
|
|
434 |
+ |
|
|
435 |
+ |
if resp.clicked() && !is_active {
|
| 401 |
436 |
|
chosen = Some(value.clone());
|
| 402 |
437 |
|
}
|
| 403 |
438 |
|
}
|
| 414 |
449 |
|
//
|
| 415 |
450 |
|
// `confirm_action_row` (above) composes these for the standard modal pattern;
|
| 416 |
451 |
|
// reach for these directly only when building a non-modal action row.
|
|
452 |
+ |
//
|
|
453 |
+ |
// All five ask for `radius_control` by name. Nothing else in the app does:
|
|
454 |
+ |
// square is the default now (see `apply_theme`), and a rounded corner is this
|
|
455 |
+ |
// codebase saying "this is a push button". A raw `ui.button` therefore comes
|
|
456 |
+ |
// out square, which is correct for the toolbar and wrong for a dialog action
|
|
457 |
+ |
// row -- reach for these rather than for `ui.button` when it is the latter.
|
| 417 |
458 |
|
|
| 418 |
459 |
|
/// Primary action button. Strong label weight.
|
| 419 |
460 |
|
pub fn primary_button(ui: &mut egui::Ui, label: &str) -> egui::Response {
|
| 420 |
|
- |
ui.add(egui::Button::new(egui::RichText::new(label).strong()))
|
|
461 |
+ |
ui.add(
|
|
462 |
+ |
egui::Button::new(egui::RichText::new(label).strong())
|
|
463 |
+ |
.corner_radius(theme::radius_control()),
|
|
464 |
+ |
)
|
| 421 |
465 |
|
}
|
| 422 |
466 |
|
|
| 423 |
467 |
|
/// Secondary / peer action button. Default weight. Use for Cancel and for any
|
| 424 |
468 |
|
/// action that isn't the primary focus of the row.
|
| 425 |
469 |
|
pub fn secondary_button(ui: &mut egui::Ui, label: &str) -> egui::Response {
|
| 426 |
|
- |
ui.button(label)
|
|
470 |
+ |
ui.add(egui::Button::new(label).corner_radius(theme::radius_control()))
|
| 427 |
471 |
|
}
|
| 428 |
472 |
|
|
| 429 |
473 |
|
/// Destructive primary action. Label rendered in `danger` so the user
|
| 430 |
474 |
|
/// reads the consequence before clicking. Used for Delete, Purge, Discard.
|
| 431 |
475 |
|
pub fn danger_button(ui: &mut egui::Ui, label: &str) -> egui::Response {
|
| 432 |
|
- |
ui.add(egui::Button::new(
|
| 433 |
|
- |
egui::RichText::new(label).color(theme::danger()),
|
| 434 |
|
- |
))
|
|
476 |
+ |
ui.add(
|
|
477 |
+ |
egui::Button::new(egui::RichText::new(label).color(theme::danger()))
|
|
478 |
+ |
.corner_radius(theme::radius_control()),
|
|
479 |
+ |
)
|
| 435 |
480 |
|
}
|
| 436 |
481 |
|
|
| 437 |
482 |
|
/// Destructive action that may be disabled (e.g. Delete-vault when only one
|
| 440 |
485 |
|
pub fn danger_button_enabled(ui: &mut egui::Ui, label: &str, enabled: bool) -> egui::Response {
|
| 441 |
486 |
|
ui.add_enabled(
|
| 442 |
487 |
|
enabled,
|
| 443 |
|
- |
egui::Button::new(egui::RichText::new(label).color(theme::danger())),
|
|
488 |
+ |
egui::Button::new(egui::RichText::new(label).color(theme::danger()))
|
|
489 |
+ |
.corner_radius(theme::radius_control()),
|
| 444 |
490 |
|
)
|
| 445 |
491 |
|
}
|
| 446 |
492 |
|
|
| 447 |
493 |
|
/// Small destructive action (per-row Remove/Delete affordances, context-menu
|
| 448 |
494 |
|
/// items inside a tighter layout). Same colouring as [`danger_button`].
|
| 449 |
495 |
|
pub fn danger_small_button(ui: &mut egui::Ui, label: &str) -> egui::Response {
|
| 450 |
|
- |
ui.add(egui::Button::new(egui::RichText::new(label).color(theme::danger())).small())
|
|
496 |
+ |
ui.add(
|
|
497 |
+ |
egui::Button::new(egui::RichText::new(label).color(theme::danger()))
|
|
498 |
+ |
.small()
|
|
499 |
+ |
.corner_radius(theme::radius_control()),
|
|
500 |
+ |
)
|
| 451 |
501 |
|
}
|
| 452 |
502 |
|
|
| 453 |
503 |
|
// --- Section headers ---------------------------------------------------------
|
| 627 |
677 |
|
/// Draw a tag as a small colored chip.
|
| 628 |
678 |
|
///
|
| 629 |
679 |
|
/// Uses custom rendering (`allocate_exact_size` + `painter()`) instead of a standard
|
| 630 |
|
- |
/// egui widget because tag chips need a specific rounded-rect background, precise
|
| 631 |
|
- |
/// font size (11pt), and hover highlighting that standard `Label` doesn't provide.
|
|
680 |
+ |
/// egui widget because tag chips need a specific background, precise font size
|
|
681 |
+ |
/// (11pt), and hover highlighting that standard `Label` doesn't provide.
|
|
682 |
+ |
///
|
|
683 |
+ |
/// Square and bevelled rather than rounded: a chip is a small raised tile here,
|
|
684 |
+ |
/// not a pill. The rounded version read as a web token, which is the one visual
|
|
685 |
+ |
/// language this app has no business borrowing from.
|
| 632 |
686 |
|
pub fn tag_chip(ui: &mut egui::Ui, tag: &str) -> egui::Response {
|
| 633 |
687 |
|
// Estimate width from character count * average glyph width + padding.
|
| 634 |
688 |
|
let (rect, response) = ui.allocate_exact_size(
|
| 648 |
702 |
|
} else {
|
| 649 |
703 |
|
theme::surface_raised()
|
| 650 |
704 |
|
};
|
| 651 |
|
- |
ui.painter().rect_filled(rect, 4.0, bg);
|
|
705 |
+ |
ui.painter()
|
|
706 |
+ |
.rect_filled(rect, theme::radius_container(), bg);
|
|
707 |
+ |
theme::bevel::paint(ui.painter(), rect, theme::bevel::Bevel::Raised);
|
| 652 |
708 |
|
ui.painter().text(
|
| 653 |
709 |
|
rect.center(),
|
| 654 |
710 |
|
egui::Align2::CENTER_CENTER,
|