summaryrefslogtreecommitdiffstats
path: root/gui/src/widgets.rs
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src/widgets.rs')
-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}