Skip to main content

blockworx/
role_picker.rs

1//! The accent-color (role) picker popup: a 3×3 grid of palette swatches shown
2//! above the selection overlay for the selected shape (block, port, route,
3//! area, or text box).
4//!
5//! The cells are
6//! [`blockworx_tools::commands::ACCENTS`] — the registry's own accent
7//! commands, so what a swatch sets is what a script naming `accent-3` sets —
8//! and each shows the colour
9//! [`blockworx_kernel::chrome::accent_display_role`]
10//! resolves for the target, which is the target's own un-accented stroke in
11//! the "no accent" cell. The swatch matching the shape's current role carries
12//! a thick active-indicator ring (palette `B07`). Clicking a swatch sets the
13//! role; clicking outside dismisses the popup.
14
15use crate::canvas::convert::IntoEgui as _;
16use blockworx_kernel::chrome::accent_display_role;
17use blockworx_paint::Base;
18use blockworx_tools::commands::ACCENTS;
19use egui::{Pos2, Sense, Stroke, StrokeKind, vec2};
20
21use crate::theme::Theme;
22use crate::tools::tool::RoleTarget;
23
24/// Outcome of showing the picker for one frame.
25pub enum RolePick {
26    /// A swatch was clicked: set the shape's role to this value (`None` = default).
27    Set(Option<u8>),
28    /// The user clicked outside the popup: dismiss it.
29    Dismiss,
30    /// Nothing happened this frame.
31    None,
32}
33
34/// Side length (points) of each swatch.
35const SWATCH: f32 = 28.0;
36
37/// Swatches per row: nine cells read as a colour grid rather than a list.
38const ROW: usize = 3;
39
40/// Show the picker with its bottom-right corner anchored at screen position
41/// `at` (just above the selection overlay), highlighting `current`. The cells
42/// are the registry's own accent commands, so the picker cannot offer a
43/// colour a script could not name; `target` is what the "no accent" cell
44/// previews the un-accented stroke of. Must be driven from app state that was
45/// set on a *previous* frame, so the click that opened the popup isn't read
46/// here as a click-outside dismiss.
47pub fn show(
48    ctx: &egui::Context,
49    at: Pos2,
50    theme: &Theme,
51    current: Option<u8>,
52    target: RoleTarget,
53) -> RolePick {
54    let mut pick = RolePick::None;
55    // Both rings come from the palette, not from a fixed grey: the cells
56    // they surround are palette colors, so a scheme change has to move the
57    // rings with them or one of the two stops reading.
58    let active_ring = theme.palette().get(Base::B07).egui();
59    let resting_ring = theme.palette().get(Base::B03).egui();
60    let area = egui::Area::new(egui::Id::new("role_picker"))
61        .order(egui::Order::Foreground)
62        .fixed_pos(at)
63        .pivot(egui::Align2::RIGHT_BOTTOM)
64        .show(ctx, |ui| {
65            egui::Frame::popup(ui.style()).show(ui, |ui| {
66                egui::Grid::new("role_picker_grid")
67                    .spacing(vec2(4.0, 4.0))
68                    .show(ui, |ui| {
69                        for (i, (_, _, value)) in ACCENTS.iter().enumerate() {
70                            let (rect, resp) =
71                                ui.allocate_exact_size(vec2(SWATCH, SWATCH), Sense::click());
72                            let role = accent_display_role(target, *value);
73                            let p = ui.painter();
74                            p.rect_filled(rect, 4.0, theme.resolve(role).egui());
75                            let (w, col) = if *value == current {
76                                (3.0, active_ring)
77                            } else {
78                                (1.0, resting_ring)
79                            };
80                            p.rect_stroke(rect, 4.0, Stroke::new(w, col), StrokeKind::Inside);
81                            if resp.clicked() {
82                                pick = RolePick::Set(*value);
83                            }
84                            if (i + 1) % ROW == 0 {
85                                ui.end_row();
86                            }
87                        }
88                    });
89            });
90        });
91    // A swatch click takes precedence; otherwise a click outside dismisses.
92    if matches!(pick, RolePick::None) && area.response.clicked_elsewhere() {
93        pick = RolePick::Dismiss;
94    }
95    pick
96}