diff options
Diffstat (limited to '')
| -rw-r--r-- | gui/src/widgets.rs | 255 |
1 files changed, 255 insertions, 0 deletions
diff --git a/gui/src/widgets.rs b/gui/src/widgets.rs new file mode 100644 index 0000000..f7d1512 --- /dev/null +++ b/gui/src/widgets.rs | |||
| @@ -0,0 +1,255 @@ | |||
| 1 | //! Small shared controls. Every button and panel comes from here so the | ||
| 2 | //! screens cannot drift apart. | ||
| 3 | |||
| 4 | use eframe::egui::{ | ||
| 5 | Align, Button, Color32, Frame, Layout, Margin, RichText, Sense, Stroke, Ui, Vec2, vec2, | ||
| 6 | }; | ||
| 7 | |||
| 8 | use crate::theme; | ||
| 9 | |||
| 10 | pub fn panel_frame() -> Frame { | ||
| 11 | Frame::new() | ||
| 12 | .fill(theme::SURFACE) | ||
| 13 | .corner_radius(theme::RADIUS_LG) | ||
| 14 | .inner_margin(Margin::same(theme::SPACE_4 as i8)) | ||
| 15 | } | ||
| 16 | |||
| 17 | pub fn inset_frame() -> Frame { | ||
| 18 | Frame::new() | ||
| 19 | .fill(theme::SURFACE_RAISED) | ||
| 20 | .corner_radius(theme::RADIUS_MD) | ||
| 21 | .inner_margin(Margin::same(theme::SPACE_3 as i8)) | ||
| 22 | } | ||
| 23 | |||
| 24 | /// The one green action on a screen. | ||
| 25 | pub fn primary(ui: &mut Ui, text: &str, enabled: bool, width: f32, height: f32) -> bool { | ||
| 26 | let fill = if enabled { | ||
| 27 | theme::WIN | ||
| 28 | } else { | ||
| 29 | theme::WIN.gamma_multiply(0.35) | ||
| 30 | }; | ||
| 31 | let response = ui.add_enabled( | ||
| 32 | enabled, | ||
| 33 | Button::new(RichText::new(text).font(theme::mono(13.0)).color(theme::BG)) | ||
| 34 | .fill(fill) | ||
| 35 | .corner_radius(theme::RADIUS_MD) | ||
| 36 | .min_size(vec2(width, height)), | ||
| 37 | ); | ||
| 38 | if response.hovered() { | ||
| 39 | ui.painter().rect_filled( | ||
| 40 | response.rect, | ||
| 41 | theme::RADIUS_MD, | ||
| 42 | Color32::WHITE.gamma_multiply(0.10), | ||
| 43 | ); | ||
| 44 | ui.ctx() | ||
| 45 | .set_cursor_icon(eframe::egui::CursorIcon::PointingHand); | ||
| 46 | } | ||
| 47 | response.clicked() | ||
| 48 | } | ||
| 49 | |||
| 50 | /// A bordered action that is not the main verb. | ||
| 51 | pub fn secondary(ui: &mut Ui, text: &str, enabled: bool, width: f32, height: f32) -> bool { | ||
| 52 | let stroke = if enabled { | ||
| 53 | theme::LINE | ||
| 54 | } else { | ||
| 55 | theme::LINE.gamma_multiply(0.5) | ||
| 56 | }; | ||
| 57 | let response = ui.add_enabled( | ||
| 58 | enabled, | ||
| 59 | Button::new( | ||
| 60 | RichText::new(text) | ||
| 61 | .font(theme::mono(12.0)) | ||
| 62 | .color(theme::TEXT_DIM), | ||
| 63 | ) | ||
| 64 | .fill(theme::SURFACE_RAISED) | ||
| 65 | .stroke(Stroke::new(1.0, stroke)) | ||
| 66 | .corner_radius(theme::RADIUS_MD) | ||
| 67 | .min_size(vec2(width, height)), | ||
| 68 | ); | ||
| 69 | if response.hovered() { | ||
| 70 | ui.painter().rect_filled( | ||
| 71 | response.rect, | ||
| 72 | theme::RADIUS_MD, | ||
| 73 | Color32::WHITE.gamma_multiply(0.06), | ||
| 74 | ); | ||
| 75 | ui.ctx() | ||
| 76 | .set_cursor_icon(eframe::egui::CursorIcon::PointingHand); | ||
| 77 | } | ||
| 78 | response.clicked() | ||
| 79 | } | ||
| 80 | |||
| 81 | /// A quiet text action, for "back" and "forget". | ||
| 82 | pub fn ghost(ui: &mut Ui, text: &str) -> bool { | ||
| 83 | let response = ui.add( | ||
| 84 | Button::new( | ||
| 85 | RichText::new(text) | ||
| 86 | .font(theme::sans(12.0)) | ||
| 87 | .color(theme::TEXT_DIM), | ||
| 88 | ) | ||
| 89 | .frame(false), | ||
| 90 | ); | ||
| 91 | if response.hovered() { | ||
| 92 | ui.painter().rect_filled( | ||
| 93 | response.rect.expand(2.0), | ||
| 94 | theme::RADIUS_SM, | ||
| 95 | Color32::WHITE.gamma_multiply(0.05), | ||
| 96 | ); | ||
| 97 | ui.ctx() | ||
| 98 | .set_cursor_icon(eframe::egui::CursorIcon::PointingHand); | ||
| 99 | } | ||
| 100 | response.clicked() | ||
| 101 | } | ||
| 102 | |||
| 103 | /// Split a game sidebar into a flexible content area and a pinned action bar, | ||
| 104 | /// so the primary action never moves as the bet slip grows. | ||
| 105 | pub fn sidebar_layout(ui: &Ui, actions_height: f32) -> (eframe::egui::Rect, eframe::egui::Rect) { | ||
| 106 | let full = ui.max_rect(); | ||
| 107 | let actions_top = (full.bottom() - actions_height).max(full.top()); | ||
| 108 | let actions = eframe::egui::Rect::from_min_max( | ||
| 109 | eframe::egui::pos2(full.left(), actions_top), | ||
| 110 | full.right_bottom(), | ||
| 111 | ); | ||
| 112 | let content = eframe::egui::Rect::from_min_max( | ||
| 113 | full.min, | ||
| 114 | eframe::egui::pos2(full.right(), (actions_top - theme::SPACE_3).max(full.top())), | ||
| 115 | ); | ||
| 116 | (content, actions) | ||
| 117 | } | ||
| 118 | |||
| 119 | pub fn square_button(ui: &mut Ui, text: &str, enabled: bool) -> bool { | ||
| 120 | ui.add_enabled( | ||
| 121 | enabled, | ||
| 122 | Button::new( | ||
| 123 | RichText::new(text) | ||
| 124 | .font(theme::mono(16.0)) | ||
| 125 | .color(theme::TEXT), | ||
| 126 | ) | ||
| 127 | .fill(theme::SURFACE_RAISED) | ||
| 128 | .stroke(Stroke::new(1.0, theme::LINE)) | ||
| 129 | .corner_radius(theme::RADIUS_MD) | ||
| 130 | .min_size(Vec2::splat(34.0)), | ||
| 131 | ) | ||
| 132 | .clicked() | ||
| 133 | } | ||
| 134 | |||
| 135 | /// `- [ 50 Kr ] +`; returns the change in the bet. | ||
| 136 | pub fn bet_control(ui: &mut Ui, bet: u64, enabled: bool) -> Option<i64> { | ||
| 137 | let mut delta = None; | ||
| 138 | ui.horizontal(|ui| { | ||
| 139 | if square_button(ui, "−", enabled) { | ||
| 140 | delta = Some(-10); | ||
| 141 | } | ||
| 142 | Frame::new() | ||
| 143 | .fill(theme::BG) | ||
| 144 | .corner_radius(theme::RADIUS_SM) | ||
| 145 | .inner_margin(Margin::symmetric( | ||
| 146 | theme::SPACE_3 as i8, | ||
| 147 | theme::SPACE_2 as i8, | ||
| 148 | )) | ||
| 149 | .show(ui, |ui| { | ||
| 150 | ui.label( | ||
| 151 | RichText::new(theme::money(bet as i64)) | ||
| 152 | .font(theme::mono(15.0)) | ||
| 153 | .color(theme::TEXT), | ||
| 154 | ); | ||
| 155 | }); | ||
| 156 | if square_button(ui, "+", enabled) { | ||
| 157 | delta = Some(10); | ||
| 158 | } | ||
| 159 | }); | ||
| 160 | delta | ||
| 161 | } | ||
| 162 | |||
| 163 | /// Label on the left, value on the right, for slips and summaries. | ||
| 164 | pub fn row(ui: &mut Ui, label: &str, value: &str, color: Color32) { | ||
| 165 | ui.horizontal(|ui| { | ||
| 166 | ui.label( | ||
| 167 | RichText::new(label) | ||
| 168 | .font(theme::sans(13.0)) | ||
| 169 | .color(theme::TEXT_DIM), | ||
| 170 | ); | ||
| 171 | ui.with_layout(Layout::right_to_left(Align::Center), |ui| { | ||
| 172 | ui.label(RichText::new(value).font(theme::mono(13.0)).color(color)); | ||
| 173 | }); | ||
| 174 | }); | ||
| 175 | } | ||
| 176 | |||
| 177 | pub fn note(ui: &mut Ui, text: &str) { | ||
| 178 | ui.label( | ||
| 179 | RichText::new(text) | ||
| 180 | .font(theme::sans(12.0)) | ||
| 181 | .color(theme::TEXT_DIM), | ||
| 182 | ); | ||
| 183 | } | ||
| 184 | |||
| 185 | pub fn error(ui: &mut Ui, text: &str) { | ||
| 186 | Frame::new() | ||
| 187 | .fill(theme::LOSS.gamma_multiply(0.12)) | ||
| 188 | .corner_radius(theme::RADIUS_SM) | ||
| 189 | .inner_margin(Margin::symmetric( | ||
| 190 | theme::SPACE_3 as i8, | ||
| 191 | theme::SPACE_2 as i8, | ||
| 192 | )) | ||
| 193 | .show(ui, |ui| { | ||
| 194 | ui.label( | ||
| 195 | RichText::new(text) | ||
| 196 | .font(theme::sans(12.0)) | ||
| 197 | .color(theme::LOSS), | ||
| 198 | ); | ||
| 199 | }); | ||
| 200 | } | ||
| 201 | |||
| 202 | /// A monospaced label that can be selected (fingerprints, addresses, URLs). | ||
| 203 | pub fn mono_text(ui: &mut Ui, text: &str, color: Color32) { | ||
| 204 | ui.add( | ||
| 205 | eframe::egui::Label::new( | ||
| 206 | RichText::new(text) | ||
| 207 | .font(theme::mono(theme::FONT_SMALL)) | ||
| 208 | .color(color), | ||
| 209 | ) | ||
| 210 | .selectable(true), | ||
| 211 | ); | ||
| 212 | } | ||
| 213 | |||
| 214 | pub fn chip_button(ui: &mut Ui, amount: u64, selected: bool, enabled: bool) -> bool { | ||
| 215 | let sense = if enabled { | ||
| 216 | Sense::click() | ||
| 217 | } else { | ||
| 218 | Sense::hover() | ||
| 219 | }; | ||
| 220 | let (rect, response) = ui.allocate_exact_size(Vec2::splat(46.0), sense); | ||
| 221 | let center = rect.center(); | ||
| 222 | let radius = 20.0; | ||
| 223 | let painter = ui.painter(); | ||
| 224 | if selected || response.hovered() { | ||
| 225 | painter.circle_stroke( | ||
| 226 | center, | ||
| 227 | radius + 3.0, | ||
| 228 | Stroke::new(if selected { 2.0 } else { 1.0 }, theme::ACCENT), | ||
| 229 | ); | ||
| 230 | } | ||
| 231 | if enabled { | ||
| 232 | super::roulette::paint_chip(painter, center, radius, amount); | ||
| 233 | } else { | ||
| 234 | super::roulette::paint_chip_preview(painter, center, radius, amount); | ||
| 235 | } | ||
| 236 | response.clicked() | ||
| 237 | } | ||
| 238 | |||
| 239 | pub fn chip_row<const N: usize>( | ||
| 240 | ui: &mut Ui, | ||
| 241 | amounts: [u64; N], | ||
| 242 | selected: u64, | ||
| 243 | enabled: bool, | ||
| 244 | ) -> Option<u64> { | ||
| 245 | let mut picked = None; | ||
| 246 | ui.horizontal(|ui| { | ||
| 247 | ui.spacing_mut().item_spacing.x = theme::SPACE_2; | ||
| 248 | for amount in amounts { | ||
| 249 | if chip_button(ui, amount, selected == amount, enabled) { | ||
| 250 | picked = Some(amount); | ||
| 251 | } | ||
| 252 | } | ||
| 253 | }); | ||
| 254 | picked | ||
| 255 | } | ||
