summaryrefslogtreecommitdiffstats
path: root/gui/src/widgets.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-01 21:09:56 +0100
committersalmonik <salmonik@tuta.io>2026-11-01 21:09:56 +0100
commitbf4cf78ab26896faf457273e37d30fc96381c4a4 (patch)
tree71d67c300b6be191016a988ed5338e15c8b1bdd3 /gui/src/widgets.rs
parentv4.1.1: pointer-first betting and visible payouts in the GUI (diff)
downloadzagan-bet-bf4cf78ab26896faf457273e37d30fc96381c4a4.tar.gz
zagan-bet-bf4cf78ab26896faf457273e37d30fc96381c4a4.zip
v4.2.0: wire the GUI to the core, transport and all four games
Phase 4. The egui frontend now shares ClientCore and the client database with the terminal: one background worker owns TLS, trust answers go through a channel, and a 10 s request deadline mirrors the terminal socket timeout. First contact, a changed key and a version mismatch are explicit screens; login can remember the credential and auto-login signs in before the login form appears. Adds the lobby with an update notice, a full-width scrollable history table, and the four games painted from View: slots with reel animation and win lines that stop at the last matching reel, roulette with the hover lattice and wheel spin, blackjack with vector-drawn suits, and a smooth crash curve. Sidebars pin their action block; leaving a live hand or round asks before forfeiting. The header balance holds the staked amount while the reels or wheel turn and only shows the payout when they stop. Refused spins cancel the animation. Live crash multipliers keep full precision for the chart; the terminal still displays two decimals. Verified live against a 4.2.0 server: register, login, auto-login, trust, all four games settling, history (50 rows), logout, disconnect, forfeit confirmation, and the version-mismatch screen. Release: 8.3 MB Linux, 6.8 MB Windows, 61 ms to window, 131 MB idle RSS.
Diffstat (limited to '')
-rw-r--r--gui/src/widgets.rs255
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
4use eframe::egui::{
5 Align, Button, Color32, Frame, Layout, Margin, RichText, Sense, Stroke, Ui, Vec2, vec2,
6};
7
8use crate::theme;
9
10pub 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
17pub 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.
25pub 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.
51pub 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".
82pub 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.
105pub 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
119pub 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.
136pub 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.
164pub 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
177pub 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
185pub 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).
203pub 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
214pub 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
239pub 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}