pad.rsannotatedpad.rssource166 lines · 5.8 KB · raw
1//! A SNES controller that lights the buttons being pressed.
2
3use std::fmt;
4
5use console::SnesButton;
6use egui::{Align2, Color32, FontId, Pos2, Rect, Sense, Vec2, pos2, vec2};
7
8/// How sure the presser is of a press: a probability, so 0 to 1.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct Confidence(f32);
11
12impl Confidence {
13    pub fn new(probability: f32) -> Option<Self> {
14        (0.0..=1.0).contains(&probability).then_some(Self(probability))
15    }
16}
17
18/// As a whole percentage, which is what fits on a button.
19impl fmt::Display for Confidence {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        write!(f, "{:.0}", self.0 * 100.0)
22    }
23}
24
25/// A button being held. A key on the keyboard is pressed without any
26/// confidence; a press Jev chose carries the probability it chose it with.
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct Press {
29    pub confidence: Option<Confidence>,
30}
31
32/// The drawing is laid out on a pad this size and scaled to the room there is.
33const DESIGN: Vec2 = vec2(260.0, 120.0);
34
35/// Height over width of what [`show`] draws, for a host that gives it a place.
36pub const ASPECT: f32 = DESIGN.y / DESIGN.x;
37
38const BODY: Color32 = Color32::from_rgb(0xC8, 0xC6, 0xCF);
39const DARK: Color32 = Color32::from_rgb(0x4B, 0x4B, 0x55);
40const PURPLE: Color32 = Color32::from_rgb(0x5B, 0x3E, 0x9E);
41const LAVENDER: Color32 = Color32::from_rgb(0xA9, 0x98, 0xD8);
42const LIT: Color32 = Color32::from_rgb(0xFF, 0xD2, 0x3C);
43const ON_LIT: Color32 = Color32::from_rgb(0x20, 0x18, 0x00);
44
45enum Shape {
46    Round { centre: Pos2, radius: f32 },
47    Bar { rect: Rect, rounding: f32 },
48}
49
50struct Part {
51    button: SnesButton,
52    shape: Shape,
53    colour: Color32,
54    /// Printed on the button when it is not lit.
55    legend: &'static str,
56    /// Where the legend or the confidence goes: the middle of the button,
57    /// except on a shoulder, most of which is behind the body.
58    label: Pos2,
59}
60
61fn bar(left: f32, top: f32, right: f32, bottom: f32) -> Rect {
62    Rect::from_min_max(pos2(left, top), pos2(right, bottom))
63}
64
65/// Shoulders first: the body is drawn over their lower edge.
66fn shoulders() -> [Part; 2] {
67    [
68        Part {
69            button: SnesButton::L,
70            shape: Shape::Bar { rect: bar(30.0, 2.0, 100.0, 34.0), rounding: 8.0 },
71            colour: DARK,
72            legend: "L",
73            label: pos2(65.0, 11.0),
74        },
75        Part {
76            button: SnesButton::R,
77            shape: Shape::Bar { rect: bar(160.0, 2.0, 230.0, 34.0), rounding: 8.0 },
78            colour: DARK,
79            legend: "R",
80            label: pos2(195.0, 11.0),
81        },
82    ]
83}
84
85fn face() -> [Part; 10] {
86    let arm = |button, rect: Rect| Part {
87        button,
88        shape: Shape::Bar { rect, rounding: 2.0 },
89        colour: DARK,
90        legend: "",
91        label: rect.center(),
92    };
93    let round = |button, x, y, colour, legend| Part {
94        button,
95        shape: Shape::Round { centre: pos2(x, y), radius: 11.0 },
96        colour,
97        legend,
98        label: pos2(x, y),
99    };
100    let pill = |button, x: f32| Part {
101        button,
102        shape: Shape::Bar { rect: bar(x - 12.0, 70.0, x + 12.0, 80.0), rounding: 5.0 },
103        colour: DARK,
104        legend: "",
105        label: pos2(x, 75.0),
106    };
107    [
108        arm(SnesButton::Up, bar(55.0, 38.0, 69.0, 60.0)),
109        arm(SnesButton::Down, bar(55.0, 74.0, 69.0, 96.0)),
110        arm(SnesButton::Left, bar(33.0, 60.0, 55.0, 74.0)),
111        arm(SnesButton::Right, bar(69.0, 60.0, 91.0, 74.0)),
112        round(SnesButton::X, 198.0, 45.0, LAVENDER, "X"),
113        round(SnesButton::A, 222.0, 67.0, PURPLE, "A"),
114        round(SnesButton::B, 198.0, 89.0, PURPLE, "B"),
115        round(SnesButton::Y, 174.0, 67.0, LAVENDER, "Y"),
116        pill(SnesButton::Select, 114.0),
117        pill(SnesButton::Start, 144.0),
118    ]
119}
120
121/// Draw the controller across the width available. A lit button shows the
122/// confidence it was pressed with, as a percentage, if it was pressed with one.
123pub fn show(ui: &mut egui::Ui, pressed: impl Fn(SnesButton) -> Option<Press>) {
124    let scale = ui.available_width() / DESIGN.x;
125    let (response, painter) = ui.allocate_painter(DESIGN * scale, Sense::hover());
126    let origin = response.rect.min;
127    let place = |at: Pos2| origin + at.to_vec2() * scale;
128    let font = FontId::proportional(9.0 * scale);
129
130    let draw = |part: &Part| {
131        let press = pressed(part.button);
132        let fill = if press.is_some() { LIT } else { part.colour };
133        match part.shape {
134            Shape::Round { centre, radius } => {
135                painter.circle_filled(place(centre), radius * scale, fill);
136            }
137            Shape::Bar { rect, rounding } => {
138                let placed = Rect::from_min_max(place(rect.min), place(rect.max));
139                painter.rect_filled(placed, rounding * scale, fill);
140            }
141        }
142        let (text, colour) = match press {
143            Some(Press { confidence: Some(confidence) }) => (confidence.to_string(), ON_LIT),
144            Some(Press { confidence: None }) => (part.legend.to_owned(), ON_LIT),
145            None => (part.legend.to_owned(), BODY),
146        };
147        painter.text(place(part.label), Align2::CENTER_CENTER, text, font.clone(), colour);
148    };
149
150    shoulders().iter().for_each(&draw);
151    let body = Rect::from_min_max(place(pos2(8.0, 20.0)), place(pos2(252.0, 114.0)));
152    painter.rect_filled(body, 47.0 * scale, BODY);
153    // The hub of the d-pad, which is not a button.
154    let hub = Rect::from_min_max(place(pos2(55.0, 60.0)), place(pos2(69.0, 74.0)));
155    painter.rect_filled(hub, 0.0, DARK);
156    face().iter().for_each(&draw);
157    for (x, name) in [(114.0, "SELECT"), (144.0, "START")] {
158        painter.text(
159            place(pos2(x, 88.0)),
160            Align2::CENTER_CENTER,
161            name,
162            FontId::proportional(6.0 * scale),
163            DARK,
164        );
165    }
166}