Skip to main content

blockworx/widget/
io_pin_picker.rs

1//! The pin-type (I/O style) picker popup: a row of three cells — Input,
2//! Input Output, Output — shown above the selection overlay for a selected
3//! pin, pin group, or port. The cell matching the selection's shared type is
4//! marked active; when the selected pins disagree (a mixed group) no cell is
5//! active. Clicking a cell sets every selected pin to that type; clicking
6//! outside dismisses the popup.
7//!
8//! Each cell is its glyph, with the word in its tooltip — the user: *"instead
9//! of having the words 'Input, Output, Input Output', use icon
10//! representations for each one."* The three glyphs live here, because this
11//! is the surface that offers all three; the overlay's trigger borrows the
12//! one it wears in every state.
13
14use egui::Pos2;
15
16use crate::shape::pin::PinDir;
17use crate::shell::glass;
18
19pub const INPUT_ICON: egui::ImageSource<'static> =
20    egui::include_image!("../../icons/icon-pin-input.svg");
21const OUTPUT_ICON: egui::ImageSource<'static> =
22    egui::include_image!("../../icons/icon-pin-output.svg");
23const INOUT_ICON: egui::ImageSource<'static> =
24    egui::include_image!("../../icons/icon-pin-inout.svg");
25
26/// The glyph for one direction: an arrow arriving at a wall is an input, one
27/// leaving for it an output, and one at each end is both.
28fn icon(dir: PinDir) -> egui::ImageSource<'static> {
29    match dir {
30        PinDir::Input => INPUT_ICON,
31        PinDir::Output => OUTPUT_ICON,
32        PinDir::InOut => INOUT_ICON,
33    }
34}
35
36/// Outcome of showing the picker for one frame.
37pub enum PinTypePick {
38    /// A cell was clicked: set every selected pin to this type.
39    Set(PinDir),
40    /// The user clicked outside the popup: dismiss it.
41    Dismiss,
42    /// Nothing happened this frame.
43    None,
44}
45
46/// The three cells in display order: the type each sets and the word its
47/// tooltip carries.
48const CELLS: [(PinDir, &str); 3] = [
49    (PinDir::Input, "Input"),
50    (PinDir::InOut, "Input Output"),
51    (PinDir::Output, "Output"),
52];
53
54/// Show the picker with its bottom-right corner anchored at `at` (just above the
55/// selection overlay), marking `current` active — the selection's shared pin
56/// type, or `None` when the selected pins disagree. Must be driven from app
57/// state set on a *previous* frame, so the click that opened the popup isn't read
58/// here as a click-outside dismiss.
59pub fn show(ctx: &egui::Context, at: Pos2, current: Option<PinDir>) -> PinTypePick {
60    let mut pick = PinTypePick::None;
61    let area = egui::Area::new(egui::Id::new("pin_type_picker"))
62        .order(egui::Order::Foreground)
63        .fixed_pos(at)
64        .pivot(egui::Align2::RIGHT_BOTTOM)
65        .show(ctx, |ui| {
66            egui::Frame::popup(ui.style()).show(ui, |ui| {
67                ui.horizontal(|ui| {
68                    for (kind, label) in CELLS {
69                        let active = current == Some(kind);
70                        let cell = egui::Button::image(glass::image(ui, icon(kind), ICON))
71                            .min_size(egui::Vec2::splat(glass::TAP))
72                            .selected(active);
73                        if ui.add(cell).on_hover_text(label).clicked() {
74                            pick = PinTypePick::Set(kind);
75                        }
76                    }
77                });
78            });
79        });
80    // A cell click takes precedence; otherwise a click outside dismisses.
81    if matches!(pick, PinTypePick::None) && area.response.clicked_elsewhere() {
82        pick = PinTypePick::Dismiss;
83    }
84    pick
85}
86
87/// The glyph inside a cell, the size every tap target in the shell draws its
88/// icon at.
89const ICON: f32 = 21.0;
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    /// Item 14: three choices, three glyphs, and the words in the tooltips.
96    /// The picker is where every direction is drawn, so a direction that
97    /// borrowed another's glyph would be two choices the eye cannot tell
98    /// apart.
99    #[test]
100    fn every_direction_has_a_glyph_of_its_own_and_a_word_to_go_with_it() {
101        let uri = |dir: PinDir| match icon(dir) {
102            egui::ImageSource::Bytes { uri, .. } => uri.to_string(),
103            _ => panic!("the picker's icons are embedded bytes"),
104        };
105        let glyphs: std::collections::HashSet<String> =
106            CELLS.iter().map(|(dir, _)| uri(*dir)).collect();
107        assert_eq!(glyphs.len(), CELLS.len(), "two directions share one glyph");
108        let words: std::collections::HashSet<&str> = CELLS.iter().map(|(_, word)| *word).collect();
109        assert_eq!(words, ["Input", "Output", "Input Output"].into());
110        assert_eq!(
111            uri(PinDir::Input),
112            match INPUT_ICON {
113                egui::ImageSource::Bytes { uri, .. } => uri.to_string(),
114                _ => unreachable!(),
115            },
116            "the overlay's trigger and the picker's Input cell have parted",
117        );
118    }
119}