diff options
Diffstat (limited to '')
| -rw-r--r-- | gui/src/roulette.rs | 522 |
1 files changed, 522 insertions, 0 deletions
diff --git a/gui/src/roulette.rs b/gui/src/roulette.rs new file mode 100644 index 0000000..22c46d6 --- /dev/null +++ b/gui/src/roulette.rs | |||
| @@ -0,0 +1,522 @@ | |||
| 1 | //! The roulette table and wheel, painted from `RouletteView`. | ||
| 2 | //! | ||
| 3 | //! Geometry is the GUI's business: the core only says which number or region a | ||
| 4 | //! chip or the cursor is on. Everything here is derived from that, so the two | ||
| 5 | //! frontends can lay the same table out differently. | ||
| 6 | |||
| 7 | use std::f32::consts::{FRAC_PI_2, TAU}; | ||
| 8 | |||
| 9 | use eframe::egui::{ | ||
| 10 | Align2, Color32, CornerRadius, Painter, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, Vec2, | ||
| 11 | pos2, vec2, | ||
| 12 | }; | ||
| 13 | use zagan_bet::client::games::roulette::Cursor; | ||
| 14 | use zagan_bet::client::view::{RoulettePhase, RouletteView}; | ||
| 15 | use zagan_bet::roulette::{BetTarget, Outside, WHEEL, color as wheel_color, is_red}; | ||
| 16 | |||
| 17 | use crate::theme; | ||
| 18 | |||
| 19 | const COLUMNS: usize = 12; | ||
| 20 | const ROWS: usize = 3; | ||
| 21 | const OUTSIDE_ORDER: [Outside; 6] = [ | ||
| 22 | Outside::Low, | ||
| 23 | Outside::Even, | ||
| 24 | Outside::Red, | ||
| 25 | Outside::Black, | ||
| 26 | Outside::Odd, | ||
| 27 | Outside::High, | ||
| 28 | ]; | ||
| 29 | |||
| 30 | const DOZEN_ROW: f32 = 0.72; | ||
| 31 | const COLUMN_ROW: f32 = 0.55; | ||
| 32 | const OUTSIDE_ROW: f32 = 0.62; | ||
| 33 | const HEIGHT_UNITS: f32 = ROWS as f32 + DOZEN_ROW + COLUMN_ROW + OUTSIDE_ROW; | ||
| 34 | const BOARD_PAD: f32 = 6.0; | ||
| 35 | |||
| 36 | /// Largest table that fits in `available`, including its felt border. | ||
| 37 | pub fn size_for(available: Vec2) -> Vec2 { | ||
| 38 | let inner = (available - Vec2::splat(BOARD_PAD * 2.0)).max(Vec2::ZERO); | ||
| 39 | let cell = (inner.x / (COLUMNS as f32 + 1.0)) | ||
| 40 | .min(inner.y / HEIGHT_UNITS) | ||
| 41 | .max(1.0); | ||
| 42 | vec2((COLUMNS as f32 + 1.0) * cell, HEIGHT_UNITS * cell) + Vec2::splat(BOARD_PAD * 2.0) | ||
| 43 | } | ||
| 44 | |||
| 45 | /// The table painted at `size`; returns the target clicked this frame. | ||
| 46 | pub fn felt(ui: &mut Ui, size: Vec2, view: &RouletteView) -> Option<BetTarget> { | ||
| 47 | let (rect, response) = ui.allocate_exact_size(size, Sense::click()); | ||
| 48 | let felt = Felt::new(rect); | ||
| 49 | let hovered = response.hover_pos().and_then(|pos| felt.hit(pos)); | ||
| 50 | let painter = ui.painter(); | ||
| 51 | |||
| 52 | painter.rect_filled(rect, theme::RADIUS_LG, theme::FELT_DARK); | ||
| 53 | |||
| 54 | for number in 0..=36u8 { | ||
| 55 | let highlighted = hovered == Some(BetTarget::Straight { number }); | ||
| 56 | felt.paint_number(painter, number, highlighted); | ||
| 57 | } | ||
| 58 | for index in 0..3 { | ||
| 59 | felt.paint_dozen( | ||
| 60 | painter, | ||
| 61 | index, | ||
| 62 | hovered == Some(BetTarget::Dozen { index: index as u8 }), | ||
| 63 | ); | ||
| 64 | } | ||
| 65 | for index in 0..3 { | ||
| 66 | felt.paint_column( | ||
| 67 | painter, | ||
| 68 | index, | ||
| 69 | hovered == Some(BetTarget::Column { index: index as u8 }), | ||
| 70 | ); | ||
| 71 | } | ||
| 72 | for (index, kind) in OUTSIDE_ORDER.iter().enumerate() { | ||
| 73 | felt.paint_outside( | ||
| 74 | painter, | ||
| 75 | index, | ||
| 76 | *kind, | ||
| 77 | hovered == Some(BetTarget::Outside { kind: *kind }), | ||
| 78 | ); | ||
| 79 | } | ||
| 80 | |||
| 81 | if view.phase == RoulettePhase::Result { | ||
| 82 | if let Some(number) = view.number { | ||
| 83 | painter.rect_stroke( | ||
| 84 | felt.number_rect(number).shrink(2.0), | ||
| 85 | theme::RADIUS_SM, | ||
| 86 | Stroke::new(2.0, theme::GOLD), | ||
| 87 | StrokeKind::Inside, | ||
| 88 | ); | ||
| 89 | } | ||
| 90 | } | ||
| 91 | |||
| 92 | felt.paint_cursor(painter, &view.cursor); | ||
| 93 | |||
| 94 | for chip in &view.bets.chips { | ||
| 95 | if let Some(center) = felt.anchor(&chip.target) { | ||
| 96 | paint_chip(painter, center, felt.cell * 0.26, chip.amount); | ||
| 97 | } | ||
| 98 | } | ||
| 99 | |||
| 100 | if response.clicked() { hovered } else { None } | ||
| 101 | } | ||
| 102 | |||
| 103 | struct Felt { | ||
| 104 | cell: f32, | ||
| 105 | origin: Pos2, | ||
| 106 | } | ||
| 107 | |||
| 108 | impl Felt { | ||
| 109 | fn new(board: Rect) -> Self { | ||
| 110 | let inner = board.shrink(BOARD_PAD); | ||
| 111 | Self { | ||
| 112 | cell: inner.width() / (COLUMNS as f32 + 1.0), | ||
| 113 | origin: inner.min, | ||
| 114 | } | ||
| 115 | } | ||
| 116 | |||
| 117 | fn number_rect(&self, number: u8) -> Rect { | ||
| 118 | debug_assert!(number <= 36); | ||
| 119 | if number == 0 { | ||
| 120 | return Rect::from_min_size(self.origin, vec2(self.cell, self.cell * ROWS as f32)); | ||
| 121 | } | ||
| 122 | let column = ((number - 1) / 3) as f32; | ||
| 123 | let row = (2 - (number - 1) % 3) as f32; | ||
| 124 | Rect::from_min_size( | ||
| 125 | self.origin + vec2(self.cell * (1.0 + column), self.cell * row), | ||
| 126 | Vec2::splat(self.cell), | ||
| 127 | ) | ||
| 128 | } | ||
| 129 | |||
| 130 | fn dozen_rect(&self, index: usize) -> Rect { | ||
| 131 | let y = self.origin.y + self.cell * ROWS as f32; | ||
| 132 | Rect::from_min_size( | ||
| 133 | pos2(self.origin.x + self.cell * (1.0 + 4.0 * index as f32), y), | ||
| 134 | vec2(self.cell * 4.0, self.cell * DOZEN_ROW), | ||
| 135 | ) | ||
| 136 | } | ||
| 137 | |||
| 138 | fn column_rect(&self, index: usize) -> Rect { | ||
| 139 | let y = self.origin.y + self.cell * (ROWS as f32 + DOZEN_ROW); | ||
| 140 | Rect::from_min_size( | ||
| 141 | pos2(self.origin.x + self.cell * (1.0 + index as f32), y), | ||
| 142 | vec2(self.cell, self.cell * COLUMN_ROW), | ||
| 143 | ) | ||
| 144 | } | ||
| 145 | |||
| 146 | fn outside_rect(&self, index: usize) -> Rect { | ||
| 147 | let y = self.origin.y + self.cell * (ROWS as f32 + DOZEN_ROW + COLUMN_ROW); | ||
| 148 | Rect::from_min_size( | ||
| 149 | pos2(self.origin.x + self.cell * (1.0 + 2.0 * index as f32), y), | ||
| 150 | vec2(self.cell * 2.0, self.cell * OUTSIDE_ROW), | ||
| 151 | ) | ||
| 152 | } | ||
| 153 | |||
| 154 | /// Where a chip sits for this target. | ||
| 155 | fn anchor(&self, target: &BetTarget) -> Option<Pos2> { | ||
| 156 | Some(match target { | ||
| 157 | BetTarget::Straight { number } => self.number_rect(*number).center(), | ||
| 158 | BetTarget::Split { a, b } => { | ||
| 159 | let a = self.number_rect(*a).center(); | ||
| 160 | let b = self.number_rect(*b).center(); | ||
| 161 | pos2((a.x + b.x) * 0.5, (a.y + b.y) * 0.5) | ||
| 162 | } | ||
| 163 | BetTarget::Corner { numbers } => { | ||
| 164 | let mut sum = Vec2::ZERO; | ||
| 165 | for number in numbers { | ||
| 166 | sum += self.number_rect(*number).center().to_vec2(); | ||
| 167 | } | ||
| 168 | (sum / numbers.len() as f32).to_pos2() | ||
| 169 | } | ||
| 170 | BetTarget::Dozen { index } => self.dozen_rect(*index as usize).center(), | ||
| 171 | BetTarget::Column { index } => self.column_rect(*index as usize).center(), | ||
| 172 | BetTarget::Outside { kind } => { | ||
| 173 | let index = OUTSIDE_ORDER | ||
| 174 | .iter() | ||
| 175 | .position(|candidate| candidate == kind)?; | ||
| 176 | self.outside_rect(index).center() | ||
| 177 | } | ||
| 178 | }) | ||
| 179 | } | ||
| 180 | |||
| 181 | /// The fine-grid point the cursor occupies over the numbers. | ||
| 182 | fn lattice(&self, x: usize, y: usize) -> Pos2 { | ||
| 183 | pos2( | ||
| 184 | self.origin.x + self.cell * (1.0 + x as f32 * 0.5), | ||
| 185 | self.origin.y + self.cell * (y as f32 * 0.5), | ||
| 186 | ) | ||
| 187 | } | ||
| 188 | |||
| 189 | fn hit(&self, pos: Pos2) -> Option<BetTarget> { | ||
| 190 | for number in 0..=36u8 { | ||
| 191 | if self.number_rect(number).contains(pos) { | ||
| 192 | return Some(BetTarget::Straight { number }); | ||
| 193 | } | ||
| 194 | } | ||
| 195 | for index in 0..3usize { | ||
| 196 | if self.dozen_rect(index).contains(pos) { | ||
| 197 | return Some(BetTarget::Dozen { index: index as u8 }); | ||
| 198 | } | ||
| 199 | if self.column_rect(index).contains(pos) { | ||
| 200 | return Some(BetTarget::Column { index: index as u8 }); | ||
| 201 | } | ||
| 202 | } | ||
| 203 | for (index, kind) in OUTSIDE_ORDER.iter().enumerate() { | ||
| 204 | if self.outside_rect(index).contains(pos) { | ||
| 205 | return Some(BetTarget::Outside { kind: *kind }); | ||
| 206 | } | ||
| 207 | } | ||
| 208 | None | ||
| 209 | } | ||
| 210 | |||
| 211 | fn paint_number(&self, painter: &Painter, number: u8, hovered: bool) { | ||
| 212 | let rect = self.number_rect(number); | ||
| 213 | let fill = if number == 0 { | ||
| 214 | theme::ROULETTE_ZERO | ||
| 215 | } else if is_red(number) { | ||
| 216 | theme::ROULETTE_RED | ||
| 217 | } else { | ||
| 218 | theme::ROULETTE_BLACK | ||
| 219 | }; | ||
| 220 | painter.rect_filled(rect, CornerRadius::ZERO, fill); | ||
| 221 | painter.rect_stroke( | ||
| 222 | rect, | ||
| 223 | CornerRadius::ZERO, | ||
| 224 | Stroke::new(1.0, theme::FELT_LINE), | ||
| 225 | StrokeKind::Inside, | ||
| 226 | ); | ||
| 227 | if hovered { | ||
| 228 | painter.rect_filled( | ||
| 229 | rect, | ||
| 230 | CornerRadius::ZERO, | ||
| 231 | Color32::WHITE.gamma_multiply(0.10), | ||
| 232 | ); | ||
| 233 | } | ||
| 234 | painter.text( | ||
| 235 | rect.center(), | ||
| 236 | Align2::CENTER_CENTER, | ||
| 237 | number, | ||
| 238 | theme::mono(self.number_font()), | ||
| 239 | theme::FELT_TEXT, | ||
| 240 | ); | ||
| 241 | } | ||
| 242 | |||
| 243 | fn paint_dozen(&self, painter: &Painter, index: usize, hovered: bool) { | ||
| 244 | let rect = self.dozen_rect(index); | ||
| 245 | self.paint_region(painter, rect, hovered); | ||
| 246 | let label = match index { | ||
| 247 | 0 => "1st 12", | ||
| 248 | 1 => "2nd 12", | ||
| 249 | _ => "3rd 12", | ||
| 250 | }; | ||
| 251 | painter.text( | ||
| 252 | rect.center(), | ||
| 253 | Align2::CENTER_CENTER, | ||
| 254 | label, | ||
| 255 | theme::sans(self.label_font()), | ||
| 256 | theme::FELT_TEXT, | ||
| 257 | ); | ||
| 258 | } | ||
| 259 | |||
| 260 | fn paint_column(&self, painter: &Painter, index: usize, hovered: bool) { | ||
| 261 | let rect = self.column_rect(index); | ||
| 262 | self.paint_region(painter, rect, hovered); | ||
| 263 | painter.text( | ||
| 264 | rect.center(), | ||
| 265 | Align2::CENTER_CENTER, | ||
| 266 | "2:1", | ||
| 267 | theme::sans(self.label_font()), | ||
| 268 | theme::FELT_TEXT, | ||
| 269 | ); | ||
| 270 | } | ||
| 271 | |||
| 272 | fn paint_outside(&self, painter: &Painter, index: usize, kind: Outside, hovered: bool) { | ||
| 273 | let rect = self.outside_rect(index); | ||
| 274 | self.paint_region(painter, rect, hovered); | ||
| 275 | let color = match kind { | ||
| 276 | Outside::Red => theme::ROULETTE_RED, | ||
| 277 | Outside::Black => theme::TEXT_DIM, | ||
| 278 | _ => theme::FELT_TEXT, | ||
| 279 | }; | ||
| 280 | painter.text( | ||
| 281 | rect.center(), | ||
| 282 | Align2::CENTER_CENTER, | ||
| 283 | kind.label(), | ||
| 284 | theme::sans(self.label_font()), | ||
| 285 | color, | ||
| 286 | ); | ||
| 287 | } | ||
| 288 | |||
| 289 | fn paint_region(&self, painter: &Painter, rect: Rect, hovered: bool) { | ||
| 290 | painter.rect_filled(rect, CornerRadius::ZERO, theme::FELT); | ||
| 291 | painter.rect_stroke( | ||
| 292 | rect, | ||
| 293 | CornerRadius::ZERO, | ||
| 294 | Stroke::new(1.0, theme::FELT_LINE), | ||
| 295 | StrokeKind::Inside, | ||
| 296 | ); | ||
| 297 | if hovered { | ||
| 298 | painter.rect_filled( | ||
| 299 | rect, | ||
| 300 | CornerRadius::ZERO, | ||
| 301 | Color32::WHITE.gamma_multiply(0.10), | ||
| 302 | ); | ||
| 303 | } | ||
| 304 | } | ||
| 305 | |||
| 306 | fn paint_cursor(&self, painter: &Painter, cursor: &Cursor) { | ||
| 307 | let stroke = Stroke::new(2.0, theme::ACCENT); | ||
| 308 | match cursor { | ||
| 309 | Cursor::Zero => outline(painter, self.number_rect(0), stroke), | ||
| 310 | Cursor::Lattice { x, y } => { | ||
| 311 | for number in cursor.numbers() { | ||
| 312 | painter.rect_filled( | ||
| 313 | self.number_rect(number), | ||
| 314 | CornerRadius::ZERO, | ||
| 315 | theme::ACCENT.gamma_multiply(0.14), | ||
| 316 | ); | ||
| 317 | } | ||
| 318 | let point = self.lattice(*x, *y); | ||
| 319 | painter.circle_filled(point, self.cell * 0.05, theme::ACCENT); | ||
| 320 | painter.circle_stroke(point, self.cell * 0.14, stroke); | ||
| 321 | } | ||
| 322 | Cursor::Dozen(index) => outline(painter, self.dozen_rect(*index), stroke), | ||
| 323 | Cursor::Column(index) => outline(painter, self.column_rect(*index), stroke), | ||
| 324 | Cursor::Outside(index) => outline(painter, self.outside_rect(*index), stroke), | ||
| 325 | } | ||
| 326 | } | ||
| 327 | |||
| 328 | fn number_font(&self) -> f32 { | ||
| 329 | (self.cell * 0.40).clamp(12.0, 22.0) | ||
| 330 | } | ||
| 331 | |||
| 332 | fn label_font(&self) -> f32 { | ||
| 333 | (self.cell * 0.24).clamp(10.0, 15.0) | ||
| 334 | } | ||
| 335 | } | ||
| 336 | |||
| 337 | fn outline(painter: &Painter, rect: Rect, stroke: Stroke) { | ||
| 338 | painter.rect_stroke( | ||
| 339 | rect.shrink(1.0), | ||
| 340 | theme::RADIUS_SM, | ||
| 341 | stroke, | ||
| 342 | StrokeKind::Inside, | ||
| 343 | ); | ||
| 344 | } | ||
| 345 | |||
| 346 | /// A single chip, also used by the sidebar's chip tray. | ||
| 347 | pub fn paint_chip(painter: &Painter, center: Pos2, radius: f32, amount: u64) { | ||
| 348 | let fill = chip_color(amount); | ||
| 349 | painter.circle_filled(center, radius, fill); | ||
| 350 | painter.circle_stroke( | ||
| 351 | center, | ||
| 352 | radius - radius * 0.18, | ||
| 353 | Stroke::new(radius * 0.10, Color32::WHITE.gamma_multiply(0.55)), | ||
| 354 | ); | ||
| 355 | let text_color = if amount <= 10 { | ||
| 356 | theme::BG | ||
| 357 | } else { | ||
| 358 | Color32::WHITE | ||
| 359 | }; | ||
| 360 | painter.text( | ||
| 361 | center, | ||
| 362 | Align2::CENTER_CENTER, | ||
| 363 | amount, | ||
| 364 | theme::mono(radius * 0.85), | ||
| 365 | text_color, | ||
| 366 | ); | ||
| 367 | } | ||
| 368 | |||
| 369 | /// Colour per denomination, as a chip tray would be stocked. | ||
| 370 | pub fn chip_color(amount: u64) -> Color32 { | ||
| 371 | match amount { | ||
| 372 | 0..=10 => Color32::from_rgb(0xd9, 0xd3, 0xc7), | ||
| 373 | 11..=25 => Color32::from_rgb(0x3f, 0x7f, 0xd1), | ||
| 374 | 26..=50 => Color32::from_rgb(0xc9, 0x4b, 0x4b), | ||
| 375 | _ => Color32::from_rgb(0x3a, 0x9d, 0x5d), | ||
| 376 | } | ||
| 377 | } | ||
| 378 | |||
| 379 | fn segment_color(number: u8) -> Color32 { | ||
| 380 | match wheel_color(number) { | ||
| 381 | "red" => theme::ROULETTE_RED, | ||
| 382 | "green" => theme::ROULETTE_ZERO, | ||
| 383 | _ => Color32::from_rgb(0x2b, 0x35, 0x3f), | ||
| 384 | } | ||
| 385 | } | ||
| 386 | |||
| 387 | /// Number currently under the pointer for this wheel rotation. | ||
| 388 | pub fn number_under(rotation: f32) -> u8 { | ||
| 389 | let segment = TAU / WHEEL.len() as f32; | ||
| 390 | let index = (-rotation / segment).round() as i64; | ||
| 391 | WHEEL[index.rem_euclid(WHEEL.len() as i64) as usize] | ||
| 392 | } | ||
| 393 | |||
| 394 | /// The wheel dial: 37 coloured pockets, a fixed pointer, and the number under | ||
| 395 | /// it. `rotation` is in radians. | ||
| 396 | pub fn wheel(ui: &mut Ui, diameter: f32, view: &RouletteView, rotation: f32) { | ||
| 397 | let (rect, _) = ui.allocate_exact_size(Vec2::splat(diameter), Sense::hover()); | ||
| 398 | let painter = ui.painter(); | ||
| 399 | let center = rect.center(); | ||
| 400 | let radius = diameter * 0.5; | ||
| 401 | |||
| 402 | painter.circle_filled(center, radius, theme::SURFACE_RAISED); | ||
| 403 | painter.circle_stroke(center, radius - 1.0, Stroke::new(1.0, theme::LINE)); | ||
| 404 | |||
| 405 | let segment = TAU / WHEEL.len() as f32; | ||
| 406 | let ring = radius * 0.80; | ||
| 407 | let dot = (radius * 0.072).max(2.0); | ||
| 408 | painter.circle_stroke( | ||
| 409 | center, | ||
| 410 | ring, | ||
| 411 | Stroke::new(dot * 2.0, Color32::from_rgb(0x1f, 0x28, 0x31)), | ||
| 412 | ); | ||
| 413 | for (index, number) in WHEEL.iter().enumerate() { | ||
| 414 | let angle = -FRAC_PI_2 + rotation + index as f32 * segment; | ||
| 415 | let pocket = center + vec2(angle.cos(), angle.sin()) * ring; | ||
| 416 | let winning = view.phase == RoulettePhase::Result && view.number == Some(*number); | ||
| 417 | let stroke = if winning { | ||
| 418 | Stroke::new(2.0, theme::GOLD) | ||
| 419 | } else { | ||
| 420 | Stroke::new(1.0, Color32::from_rgb(0x3a, 0x46, 0x52)) | ||
| 421 | }; | ||
| 422 | painter.circle( | ||
| 423 | pocket, | ||
| 424 | if winning { dot * 1.25 } else { dot }, | ||
| 425 | segment_color(*number), | ||
| 426 | stroke, | ||
| 427 | ); | ||
| 428 | } | ||
| 429 | |||
| 430 | // A fixed pointer at the top, marking the winning pocket. | ||
| 431 | let tip = center + vec2(0.0, -radius + dot * 0.30); | ||
| 432 | painter.add(Shape::convex_polygon( | ||
| 433 | vec![ | ||
| 434 | tip, | ||
| 435 | tip + vec2(-dot * 0.9, -dot * 1.4), | ||
| 436 | tip + vec2(dot * 0.9, -dot * 1.4), | ||
| 437 | ], | ||
| 438 | theme::TEXT, | ||
| 439 | Stroke::NONE, | ||
| 440 | )); | ||
| 441 | |||
| 442 | let inner = radius * 0.52; | ||
| 443 | painter.circle_filled(center, inner, theme::SURFACE); | ||
| 444 | painter.circle_stroke(center, inner, Stroke::new(1.0, theme::LINE)); | ||
| 445 | |||
| 446 | let displayed = match view.phase { | ||
| 447 | RoulettePhase::Result => view.number, | ||
| 448 | RoulettePhase::Spinning => Some(number_under(rotation)), | ||
| 449 | RoulettePhase::Placing => None, | ||
| 450 | }; | ||
| 451 | match displayed { | ||
| 452 | Some(number) => { | ||
| 453 | let color = match wheel_color(number) { | ||
| 454 | "red" => theme::ROULETTE_RED, | ||
| 455 | "green" => theme::WIN, | ||
| 456 | _ => theme::TEXT, | ||
| 457 | }; | ||
| 458 | painter.text( | ||
| 459 | center, | ||
| 460 | Align2::CENTER_CENTER, | ||
| 461 | number, | ||
| 462 | theme::mono(inner * 0.85), | ||
| 463 | color, | ||
| 464 | ); | ||
| 465 | } | ||
| 466 | None => { | ||
| 467 | painter.text( | ||
| 468 | center, | ||
| 469 | Align2::CENTER_CENTER, | ||
| 470 | "—", | ||
| 471 | theme::mono(inner * 0.55), | ||
| 472 | theme::TEXT_FAINT, | ||
| 473 | ); | ||
| 474 | } | ||
| 475 | } | ||
| 476 | } | ||
| 477 | |||
| 478 | /// "Straight 17", "Corner 17/18/20/21", "Red". | ||
| 479 | pub fn target_name(target: &BetTarget) -> String { | ||
| 480 | match target { | ||
| 481 | BetTarget::Straight { number } => format!("Straight {number}"), | ||
| 482 | BetTarget::Split { a, b } => format!("Split {a}/{b}"), | ||
| 483 | BetTarget::Corner { numbers } => format!( | ||
| 484 | "Corner {}/{}/{}/{}", | ||
| 485 | numbers[0], numbers[1], numbers[2], numbers[3] | ||
| 486 | ), | ||
| 487 | BetTarget::Dozen { index } => match index { | ||
| 488 | 0 => "1st 12".to_string(), | ||
| 489 | 1 => "2nd 12".to_string(), | ||
| 490 | _ => "3rd 12".to_string(), | ||
| 491 | }, | ||
| 492 | BetTarget::Column { index } => format!("Column {}", index + 1), | ||
| 493 | BetTarget::Outside { kind } => title_case(kind.label()), | ||
| 494 | } | ||
| 495 | } | ||
| 496 | |||
| 497 | fn title_case(text: &str) -> String { | ||
| 498 | let mut characters = text.chars(); | ||
| 499 | match characters.next() { | ||
| 500 | Some(first) => { | ||
| 501 | first.to_uppercase().collect::<String>() + &characters.as_str().to_lowercase() | ||
| 502 | } | ||
| 503 | None => String::new(), | ||
| 504 | } | ||
| 505 | } | ||
| 506 | |||
| 507 | /// Small coloured dot for a result strip. | ||
| 508 | pub fn paint_result_dot(painter: &Painter, center: Pos2, radius: f32, number: u8) { | ||
| 509 | let color = match wheel_color(number) { | ||
| 510 | "red" => theme::ROULETTE_RED, | ||
| 511 | "green" => theme::ROULETTE_ZERO, | ||
| 512 | _ => theme::ROULETTE_BLACK, | ||
| 513 | }; | ||
| 514 | painter.circle_filled(center, radius, color); | ||
| 515 | painter.text( | ||
| 516 | center, | ||
| 517 | Align2::CENTER_CENTER, | ||
| 518 | number, | ||
| 519 | theme::mono(radius * 0.95), | ||
| 520 | theme::FELT_TEXT, | ||
| 521 | ); | ||
| 522 | } | ||
