1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//! `Canvas` primitive `AppLogic` ([`tui_canvas`] / [`gtk_canvas`]).
//!
//! A custom gauge the 40 other shipped primitives don't cover — exactly the
//! scenario `Canvas`'s own module doc names as the escape hatch's
//! reason to exist: a track ([`DrawOp::Rect`]), a proportional fill
//! ([`DrawOp::Rect`]), a 50% tick mark ([`DrawOp::Line`]), and a
//! percentage label painted on top ([`DrawOp::TextRun`]) — all through
//! one `Backend::draw_canvas` call, on every backend.
//!
//! Controls:
//! - `+` / Up raise the value by 10
//! - `-` / Down lower the value by 10
//! - click inside the gauge jump to that position's value
//! - q / Esc quit
use quadraui::{
AppLogic, Backend, Canvas, CanvasHit, Color, DrawOp, InteractionState, Key, MouseButton,
NamedKey, Point, Reaction, Rect, StatusBar, StatusBarSegment, UiEvent, WidgetId,
};
pub struct CanvasApp {
value: u8,
}
impl CanvasApp {
pub fn new() -> Self {
Self { value: 30 }
}
/// The gauge's own rect, in the active backend's native units — a
/// margin of one line-height on every side, same "size off
/// `line_height`, not a literal constant" convention as
/// `examples/common/image_app.rs`'s `bar_rects`.
fn gauge_rect(&self, backend: &dyn Backend) -> Rect {
let viewport = backend.viewport();
let lh = backend.line_height();
let margin = lh;
Rect::new(
margin,
lh * 2.0,
(viewport.width - margin * 2.0).max(0.0),
lh,
)
}
/// Build the gauge's `DrawOp`s, in LOCAL coordinates (`(0, 0)` is
/// the gauge rect's own top-left corner — `Canvas`'s module doc's
/// "Coordinate frame" section).
fn gauge_canvas(&self, rect: Rect) -> Canvas {
let w = rect.width;
let h = rect.height;
let fill_w = (w * self.value as f32 / 100.0).clamp(0.0, w);
let track_color = Color::rgb(60, 60, 60);
let fill_color = Color::rgb(40, 160, 90);
let tick_color = Color::rgb(220, 220, 220);
let text_color = Color::rgb(255, 255, 255);
Canvas {
id: WidgetId::new("gauge"),
ops: vec![
DrawOp::Rect {
rect: Rect::new(0.0, 0.0, w, h),
color: track_color,
},
DrawOp::Rect {
rect: Rect::new(0.0, 0.0, fill_w, h),
color: fill_color,
},
DrawOp::Line {
from: Point::new(w / 2.0, 0.0),
to: Point::new(w / 2.0, h),
color: tick_color,
stroke_width: 1.0,
},
DrawOp::TextRun {
rect: Rect::new(2.0, 0.0, w.max(1.0), h),
text: format!("{}%", self.value),
color: text_color,
},
],
}
}
fn status_bar(&self) -> StatusBar {
StatusBar {
id: WidgetId::new("status"),
left_segments: vec![StatusBarSegment {
text: format!(
" gauge: {}% — click it or +/-/arrows to adjust, q to quit ",
self.value
),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(40, 80, 120),
bold: false,
action_id: None,
}],
right_segments: vec![],
}
}
}
impl Default for CanvasApp {
fn default() -> Self {
Self::new()
}
}
impl AppLogic for CanvasApp {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
let viewport = backend.viewport();
let lh = backend.line_height();
let rect = self.gauge_rect(backend);
let _ = backend.draw_canvas(rect, &self.gauge_canvas(rect));
let status_rect = Rect::new(0.0, viewport.height - lh, viewport.width, lh);
let _ = backend.draw_status_bar_interactive(
status_rect,
&self.status_bar(),
&InteractionState::new(),
);
}
fn handle(&mut self, event: UiEvent, backend: &mut dyn Backend) -> Reaction {
match event {
UiEvent::KeyPressed {
key: Key::Char('q'),
..
}
| UiEvent::KeyPressed {
key: Key::Named(NamedKey::Escape),
..
} => Reaction::Exit,
UiEvent::KeyPressed {
key: Key::Char('+'),
..
}
| UiEvent::KeyPressed {
key: Key::Named(NamedKey::Up),
..
} => {
self.value = self.value.saturating_add(10).min(100);
Reaction::Redraw
}
UiEvent::KeyPressed {
key: Key::Char('-'),
..
}
| UiEvent::KeyPressed {
key: Key::Named(NamedKey::Down),
..
} => {
self.value = self.value.saturating_sub(10);
Reaction::Redraw
}
UiEvent::MouseDown {
button: MouseButton::Left,
position,
..
} => {
let rect = self.gauge_rect(backend);
// Same rect both `render` and this handler build the
// canvas from — `Backend::canvas_layout` (the no-paint
// twin) so paint and click-routing can never disagree,
// the same contract every other primitive's `*_layout`
// method carries.
let layout = backend.canvas_layout(rect, &self.gauge_canvas(rect));
if layout.hit_test(position.x, position.y) == CanvasHit::Inside {
let frac = ((position.x - layout.bounds.x) / layout.bounds.width.max(1.0))
.clamp(0.0, 1.0);
self.value = (frac * 100.0).round() as u8;
return Reaction::Redraw;
}
Reaction::Continue
}
UiEvent::WindowResized { .. } => Reaction::Redraw,
_ => Reaction::Continue,
}
}
}