1use serde::{Deserialize, Serialize};
18use std::cell::{Cell, RefCell};
19
20pub const UI_HEIGHT: f32 = 720.0;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
25pub enum Align {
26 #[default]
28 Left,
29 Center,
31 Right,
33}
34
35#[allow(missing_docs)]
37#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
38pub enum UiCmd {
39 Panel { rect: [f32; 4], color: [f32; 4], radius: f32, outline: [f32; 4] },
41 Text { pos: [f32; 2], text: String, size: f32, color: [f32; 4], font: String, align: Align, wrap: f32 },
43 Image { rect: [f32; 4], path: String, tint: [f32; 4] },
45 Button { id: String, rect: [f32; 4], text: String, style: ButtonStyle },
47 Slider { id: String, rect: [f32; 4], value: f32, style: ButtonStyle },
49}
50
51#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
53pub struct ButtonStyle {
54 pub font: String,
56 pub size: f32,
58 pub text: [f32; 4],
60 pub background: [f32; 4],
62 pub hover: [f32; 4],
64 pub outline: [f32; 4],
66 pub radius: f32,
68 pub align: Align,
70}
71
72impl Default for ButtonStyle {
73 fn default() -> Self {
74 Self { font: String::new(), size: 22.0, text: [0.92, 0.9, 0.85, 1.0], background: [0.0, 0.0, 0.0, 0.0], hover: [0.85, 0.12, 0.1, 0.35], outline: [0.0; 4], radius: 4.0, align: Align::Center }
75 }
76}
77
78#[derive(Default, Clone)]
80pub struct Canvas {
81 cmds: RefCell<Vec<UiCmd>>,
82 clicked: RefCell<Vec<String>>,
83 hovered: RefCell<Vec<String>>,
84 changed: RefCell<Vec<(String, f32)>>,
86 width: Cell<f32>,
87 style: RefCell<ButtonStyle>,
88 mouse: Cell<[f32; 2]>,
90 wants_mouse: Cell<bool>,
92}
93
94impl Canvas {
95 pub fn width(&self) -> f32 {
97 let w = self.width.get();
98 if w > 0.0 { w } else { UI_HEIGHT * 16.0 / 9.0 }
99 }
100 pub fn set_style(&self, style: ButtonStyle) {
102 *self.style.borrow_mut() = style;
103 }
104 pub fn style(&self) -> ButtonStyle {
106 self.style.borrow().clone()
107 }
108 pub fn panel(&self, rect: [f32; 4], color: [f32; 4], radius: f32) {
110 self.cmds.borrow_mut().push(UiCmd::Panel { rect, color, radius, outline: [0.0; 4] });
111 }
112 pub fn frame(&self, rect: [f32; 4], color: [f32; 4], outline: [f32; 4], radius: f32) {
114 self.cmds.borrow_mut().push(UiCmd::Panel { rect, color, radius, outline });
115 }
116 pub fn text(&self, text: &str, pos: [f32; 2], size: f32, color: [f32; 4], font: &str, align: Align) {
118 self.cmds.borrow_mut().push(UiCmd::Text { pos, text: text.to_string(), size, color, font: font.to_string(), align, wrap: 0.0 });
119 }
120 pub fn paragraph(&self, text: &str, pos: [f32; 2], width: f32, size: f32, color: [f32; 4], font: &str) {
122 self.cmds.borrow_mut().push(UiCmd::Text { pos, text: text.to_string(), size, color, font: font.to_string(), align: Align::Left, wrap: width });
123 }
124 pub fn image(&self, path: &str, rect: [f32; 4], tint: [f32; 4]) {
126 self.cmds.borrow_mut().push(UiCmd::Image { rect, path: path.to_string(), tint });
127 }
128 pub fn button(&self, id: &str, rect: [f32; 4], text: &str) -> bool {
130 let style = self.style.borrow().clone();
131 self.cmds.borrow_mut().push(UiCmd::Button { id: id.to_string(), rect, text: text.to_string(), style });
132 self.wants_mouse.set(true);
133 self.clicked.borrow().iter().any(|c| c == id)
134 }
135 pub fn hovered(&self, id: &str) -> bool {
137 self.hovered.borrow().iter().any(|c| c == id)
138 }
139 pub fn slider(&self, id: &str, rect: [f32; 4], value: f32) -> f32 {
141 let style = self.style.borrow().clone();
142 let v = self.changed.borrow().iter().rev().find(|(i, _)| i == id).map_or(value, |(_, v)| *v);
143 self.cmds.borrow_mut().push(UiCmd::Slider { id: id.to_string(), rect, value: v.clamp(0.0, 1.0), style });
144 self.wants_mouse.set(true);
145 v.clamp(0.0, 1.0)
146 }
147 pub fn toggle(&self, id: &str, rect: [f32; 4], label: &str, on: bool) -> bool {
149 let clicked = self.button(id, rect, &format!("{label}: {}", if on { "ON" } else { "OFF" }));
150 on ^ clicked
151 }
152 pub fn mouse(&self) -> [f32; 2] {
154 self.mouse.get()
155 }
156
157 pub fn commands(&self) -> Vec<UiCmd> {
160 self.cmds.borrow().clone()
161 }
162 pub fn wants_mouse(&self) -> bool {
164 self.wants_mouse.get()
165 }
166 pub fn begin(&self) {
168 self.cmds.borrow_mut().clear();
169 self.wants_mouse.set(false);
170 }
171 pub fn feedback(&self, clicked: Vec<String>, hovered: Vec<String>, changed: Vec<(String, f32)>, width: f32, mouse: [f32; 2]) {
173 *self.clicked.borrow_mut() = clicked;
174 *self.hovered.borrow_mut() = hovered;
175 *self.changed.borrow_mut() = changed;
176 self.width.set(width);
177 self.mouse.set(mouse);
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn buttons_report_clicks_one_frame_later() {
187 let c = Canvas::default();
188 assert!(!c.button("a", [0.0; 4], "A"));
189 c.feedback(vec!["a".into()], vec!["a".into()], vec![("vol".into(), 0.3)], 1280.0, [1.0, 2.0]);
190 c.begin();
191 assert!(c.button("a", [0.0; 4], "A"));
192 assert!(c.hovered("a"));
193 assert_eq!(c.slider("vol", [0.0; 4], 0.8), 0.3);
194 assert!(c.toggle("t", [0.0; 4], "VHS", true));
195 assert_eq!(c.width(), 1280.0);
196 assert!(c.wants_mouse());
197 }
198}