//! Small shared controls. Every button and panel comes from here so the //! screens cannot drift apart. use eframe::egui::{ Align, Button, Color32, Frame, Layout, Margin, RichText, Sense, Stroke, Ui, Vec2, vec2, }; use crate::theme; pub fn panel_frame() -> Frame { Frame::new() .fill(theme::SURFACE) .corner_radius(theme::RADIUS_LG) .inner_margin(Margin::same(theme::SPACE_4 as i8)) } pub fn inset_frame() -> Frame { Frame::new() .fill(theme::SURFACE_RAISED) .corner_radius(theme::RADIUS_MD) .inner_margin(Margin::same(theme::SPACE_3 as i8)) } /// The one green action on a screen. pub fn primary(ui: &mut Ui, text: &str, enabled: bool, width: f32, height: f32) -> bool { let fill = if enabled { theme::WIN } else { theme::WIN.gamma_multiply(0.35) }; let response = ui.add_enabled( enabled, Button::new(RichText::new(text).font(theme::mono(13.0)).color(theme::BG)) .fill(fill) .corner_radius(theme::RADIUS_MD) .min_size(vec2(width, height)), ); if response.hovered() { ui.painter().rect_filled( response.rect, theme::RADIUS_MD, Color32::WHITE.gamma_multiply(0.10), ); ui.ctx() .set_cursor_icon(eframe::egui::CursorIcon::PointingHand); } response.clicked() } /// A bordered action that is not the main verb. pub fn secondary(ui: &mut Ui, text: &str, enabled: bool, width: f32, height: f32) -> bool { let stroke = if enabled { theme::LINE } else { theme::LINE.gamma_multiply(0.5) }; let response = ui.add_enabled( enabled, Button::new( RichText::new(text) .font(theme::mono(12.0)) .color(theme::TEXT_DIM), ) .fill(theme::SURFACE_RAISED) .stroke(Stroke::new(1.0, stroke)) .corner_radius(theme::RADIUS_MD) .min_size(vec2(width, height)), ); if response.hovered() { ui.painter().rect_filled( response.rect, theme::RADIUS_MD, Color32::WHITE.gamma_multiply(0.06), ); ui.ctx() .set_cursor_icon(eframe::egui::CursorIcon::PointingHand); } response.clicked() } /// A quiet text action, for "back" and "forget". pub fn ghost(ui: &mut Ui, text: &str) -> bool { let response = ui.add( Button::new( RichText::new(text) .font(theme::sans(12.0)) .color(theme::TEXT_DIM), ) .frame(false), ); if response.hovered() { ui.painter().rect_filled( response.rect.expand(2.0), theme::RADIUS_SM, Color32::WHITE.gamma_multiply(0.05), ); ui.ctx() .set_cursor_icon(eframe::egui::CursorIcon::PointingHand); } response.clicked() } /// Split a game sidebar into a flexible content area and a pinned action bar, /// so the primary action never moves as the bet slip grows. pub fn sidebar_layout(ui: &Ui, actions_height: f32) -> (eframe::egui::Rect, eframe::egui::Rect) { let full = ui.max_rect(); let actions_top = (full.bottom() - actions_height).max(full.top()); let actions = eframe::egui::Rect::from_min_max( eframe::egui::pos2(full.left(), actions_top), full.right_bottom(), ); let content = eframe::egui::Rect::from_min_max( full.min, eframe::egui::pos2(full.right(), (actions_top - theme::SPACE_3).max(full.top())), ); (content, actions) } pub fn square_button(ui: &mut Ui, text: &str, enabled: bool) -> bool { ui.add_enabled( enabled, Button::new( RichText::new(text) .font(theme::mono(16.0)) .color(theme::TEXT), ) .fill(theme::SURFACE_RAISED) .stroke(Stroke::new(1.0, theme::LINE)) .corner_radius(theme::RADIUS_MD) .min_size(Vec2::splat(34.0)), ) .clicked() } /// `- [ 50 Kr ] +`; returns the change in the bet. pub fn bet_control(ui: &mut Ui, bet: u64, enabled: bool) -> Option { let mut delta = None; ui.horizontal(|ui| { if square_button(ui, "−", enabled) { delta = Some(-10); } Frame::new() .fill(theme::BG) .corner_radius(theme::RADIUS_SM) .inner_margin(Margin::symmetric( theme::SPACE_3 as i8, theme::SPACE_2 as i8, )) .show(ui, |ui| { ui.label( RichText::new(theme::money(bet as i64)) .font(theme::mono(15.0)) .color(theme::TEXT), ); }); if square_button(ui, "+", enabled) { delta = Some(10); } }); delta } /// Label on the left, value on the right, for slips and summaries. pub fn row(ui: &mut Ui, label: &str, value: &str, color: Color32) { ui.horizontal(|ui| { ui.label( RichText::new(label) .font(theme::sans(13.0)) .color(theme::TEXT_DIM), ); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.label(RichText::new(value).font(theme::mono(13.0)).color(color)); }); }); } pub fn note(ui: &mut Ui, text: &str) { ui.label( RichText::new(text) .font(theme::sans(12.0)) .color(theme::TEXT_DIM), ); } pub fn error(ui: &mut Ui, text: &str) { Frame::new() .fill(theme::LOSS.gamma_multiply(0.12)) .corner_radius(theme::RADIUS_SM) .inner_margin(Margin::symmetric( theme::SPACE_3 as i8, theme::SPACE_2 as i8, )) .show(ui, |ui| { ui.label( RichText::new(text) .font(theme::sans(12.0)) .color(theme::LOSS), ); }); } /// A monospaced label that can be selected (fingerprints, addresses, URLs). pub fn mono_text(ui: &mut Ui, text: &str, color: Color32) { ui.add( eframe::egui::Label::new( RichText::new(text) .font(theme::mono(theme::FONT_SMALL)) .color(color), ) .selectable(true), ); } pub fn chip_button(ui: &mut Ui, amount: u64, selected: bool, enabled: bool) -> bool { let sense = if enabled { Sense::click() } else { Sense::hover() }; let (rect, response) = ui.allocate_exact_size(Vec2::splat(46.0), sense); let center = rect.center(); let radius = 20.0; let painter = ui.painter(); if selected || response.hovered() { painter.circle_stroke( center, radius + 3.0, Stroke::new(if selected { 2.0 } else { 1.0 }, theme::ACCENT), ); } if enabled { super::roulette::paint_chip(painter, center, radius, amount); } else { super::roulette::paint_chip_preview(painter, center, radius, amount); } response.clicked() } pub fn chip_row( ui: &mut Ui, amounts: [u64; N], selected: u64, enabled: bool, ) -> Option { let mut picked = None; ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = theme::SPACE_2; for amount in amounts { if chip_button(ui, amount, selected == amount, enabled) { picked = Some(amount); } } }); picked }