Skip to main content

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