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}