graphic design tool
git clone https://git.lucas.co/cce-designer.git
src/playbar.rs (10.9K)
1 //! The playbar pane: a full-width animation-transport strip docked below the
2 //! other panes (`PLAYBAR_IDX`). App-owned on the narrow traits wrapped in
3 //! `Adapted<Playbar>`, like `Viewport3D`. Unlike the other panes it paints
4 //! through the modern `paint()` path — the designer's render walk special-cases
5 //! `PLAYBAR_IDX` to `paint_self` (geometry AND text) instead of the legacy
6 //! flat views, and skips it in `append_frame_text` so the text isn't doubled.
7
8 use cce_ui::colors;
9 use cce_ui::scene::layout::Rect;
10 use cce_ui::scene::paint::{Cap, PaintCtx};
11 use cce_ui::widget::*;
12
13 #[derive(Debug, Clone)]
14 pub struct Playbar {
15 pub playing: bool,
16 /// Playback direction while `playing`: reverse runs the frame counter
17 /// down and wraps start→end. Simnets re-solve from their seed on every
18 /// backward frame (steps are not invertible), exactly like scrubbing.
19 pub reversed: bool,
20 pub current_frame: f32,
21 pub start_frame: f32,
22 pub end_frame: f32,
23 pub fps: f32,
24 dragging: bool,
25 }
26
27 const PAD: f32 = 8.0;
28 /// Width of the play/pause button box (square-ish, clamped to pane height).
29 const BTN_W: f32 = 28.0;
30 /// Width reserved right of the track for the frame readout.
31 const READOUT_W: f32 = 110.0;
32
33 impl Playbar {
34 pub fn new() -> Adapted<Playbar> {
35 Adapted::new(Self {
36 playing: false,
37 reversed: false,
38 current_frame: 1.0,
39 start_frame: 1.0,
40 end_frame: 240.0,
41 fps: 24.0,
42 dragging: false,
43 })
44 }
45
46 fn button_rect(&self, rect: Rect) -> Rect {
47 let s = (rect.height - 2.0 * PAD).max(12.0).min(BTN_W);
48 Rect { x: rect.x + PAD, y: rect.y + (rect.height - s) * 0.5, width: s, height: s }
49 }
50
51 fn track_rect(&self, rect: Rect) -> Rect {
52 let b = self.button_rect(rect);
53 let x = b.x + b.width + PAD;
54 let h = (rect.height - 2.0 * PAD).max(8.0).min(16.0);
55 Rect {
56 x,
57 y: rect.y + (rect.height - h) * 0.5,
58 width: (rect.x + rect.width - READOUT_W - PAD - x).max(20.0),
59 height: h,
60 }
61 }
62
63 /// The playhead's normalized position over the frame range.
64 fn t(&self) -> f32 {
65 let range = self.end_frame - self.start_frame;
66 if range <= 0.0 {
67 0.0
68 } else {
69 ((self.current_frame - self.start_frame) / range).clamp(0.0, 1.0)
70 }
71 }
72
73 fn scrub_to(&mut self, px: f32, track: Rect) {
74 let t = ((px - track.x) / track.width.max(1.0)).clamp(0.0, 1.0);
75 self.current_frame = (self.start_frame + t * (self.end_frame - self.start_frame)).round();
76 }
77 }
78
79 impl Layout for Playbar {}
80
81 impl Paint for Playbar {
82 /// Subtree painter: `paint` authors the complete pane — geometry and text —
83 /// so its Text prims pass through `paint_self` verbatim instead of the
84 /// single-font own-labels re-derivation.
85 fn paints_own_subtree(&self) -> bool {
86 true
87 }
88
89 /// The pane IS its own plate, exactly the ParametersBg contract: the
90 /// parameter plate's fill — tint, opacity, and blur-behind marker
91 /// (`param_plate_fill`) — so it tracks the configured plate tint
92 /// (`style.surface.param.color`) with the other panes, where the old
93 /// hand-rolled PARAM_BG copy froze this pane at the built-in default.
94 fn color(&self) -> [f32; 4] {
95 colors::param_plate_fill()
96 }
97
98 fn solid_border(&self) -> Option<([f32; 4], f32)> {
99 colors::plate_border_color().map(|bc| (bc, colors::plate_border_thickness()))
100 }
101
102 fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
103 let r = cce_ui::layout::plate_corner_radius();
104 if r > 0.0 {
105 Some((r, (true, true, true, true)))
106 } else {
107 None
108 }
109 }
110
111 fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
112 if rect.width <= 0.0 || rect.height <= 0.0 {
113 return;
114 }
115 let relief = cce_ui::layout::control_relief();
116 let accent = colors::highlight_primary_color();
117
118 // Play/pause button: raised plate under the DE relief styling (the
119 // Button transparent-fill degradation — edges only, the pane plate is
120 // the face), flat outline otherwise.
121 let b = self.button_rect(rect);
122 let br = cce_ui::layout::button_corner_radius().min(b.width * 0.5);
123 if relief {
124 let depth = cce_ui::layout::bevel_width().min(b.height * 0.2);
125 ctx.boss(b, (br, br, br, br), depth);
126 } else {
127 ctx.border(b, (br, br, br, br), [0.0; 4], [0.35, 0.35, 0.42, 0.9], 1.0);
128 }
129 let icon = [0.85, 0.86, 0.90, 0.95];
130 if self.playing {
131 // Pause: two bars.
132 let bw = b.width * 0.16;
133 let bh = b.height * 0.44;
134 let by = b.y + (b.height - bh) * 0.5;
135 ctx.quad(Rect { x: b.x + b.width * 0.32 - bw * 0.5, y: by, width: bw, height: bh }, icon);
136 ctx.quad(Rect { x: b.x + b.width * 0.68 - bw * 0.5, y: by, width: bw, height: bh }, icon);
137 } else {
138 // Play: triangle outline (no filled-triangle prim; the DE's line
139 // aesthetic reads fine here).
140 let (cx, cy) = (b.x + b.width * 0.54, b.y + b.height * 0.5);
141 let r = b.width * 0.24;
142 let (x0, y0) = (cx - r * 0.6, cy - r);
143 let (x1, y1) = (cx - r * 0.6, cy + r);
144 let (x2, y2) = (cx + r, cy);
145 ctx.vector(x0, y0, x1, y1, 1.5, icon, Cap::Round);
146 ctx.vector(x1, y1, x2, y2, 1.5, icon, Cap::Round);
147 ctx.vector(x2, y2, x0, y0, 1.5, icon, Cap::Round);
148 }
149
150 // Timeline track: the toolkit's band (the one slider style) — a band the
151 // width of the track with its swell at the playhead, in its own shaded
152 // well, drawn by the Slider's painter.
153 let track = self.track_rect(rect);
154 let px = track.x + self.t() * track.width;
155 let band_color = if self.dragging { colors::slider_thumb_drag() } else { colors::slider_thumb() };
156 cce_ui::widget::input::slider::paint_band_shape(
157 ctx,
158 track.x,
159 track.width,
160 track.y + track.height * 0.5,
161 band_color,
162 &|x| cce_ui::widget::input::slider::band_profile(track.x, track.width, track.height, x, &[px], None),
163 );
164
165 // Tick marks: frame steps on the 1-2-5 ladder, grown until minors sit
166 // >=6px apart. Every 5th step is a major — taller, brighter, and
167 // labeled with its frame number when the pane has room below the
168 // track and majors aren't crowded.
169 let range = (self.end_frame - self.start_frame).max(1.0);
170 let ppf = track.width / range;
171 let mut step = 1.0f32;
172 let cycle = [2.0f32, 2.5, 2.0];
173 let mut ci = 0;
174 while step * ppf < 6.0 {
175 step *= cycle[ci % 3];
176 ci += 1;
177 }
178 let major = step * 5.0;
179 let minor_col = [0.82, 0.84, 0.90, 0.30];
180 let major_col = [0.87, 0.89, 0.94, 0.55];
181 let below = rect.y + rect.height - (track.y + track.height);
182 let label_room = below >= 14.0 && major * ppf >= 34.0;
183 let mut f = (self.start_frame / step).ceil() * step;
184 while f <= self.end_frame + 0.001 {
185 let x = track.x + ((f - self.start_frame) / range) * track.width;
186 let is_major = (f / major - (f / major).round()).abs() < 1e-3;
187 if is_major {
188 ctx.quad(Rect { x: x - 0.5, y: track.y, width: 1.0, height: track.height + 3.0 }, major_col);
189 if label_room && x + 24.0 <= track.x + track.width {
190 ctx.text(format!("{}", f.round() as i64), x + 3.0, track.y + track.height + 2.0, 9.0, [0x8a, 0x8a, 0x96]);
191 }
192 } else {
193 ctx.quad(
194 Rect { x: x - 0.5, y: track.y + track.height * 0.45, width: 1.0, height: track.height * 0.55 },
195 minor_col,
196 );
197 }
198 f += step;
199 }
200
201 // Playhead: a full-height line over the swell, in the DE accent.
202 ctx.quad(Rect { x: px - 1.0, y: track.y - 3.0, width: 2.0, height: track.height + 6.0 }, accent);
203
204 // Frame readout.
205 let text = format!("{:>4} / {}", self.current_frame.round() as i64, self.end_frame.round() as i64);
206 let font_size = 12.0;
207 let tx = rect.x + rect.width - READOUT_W;
208 let ty = cce_ui::layout::align_text_y(rect.y, rect.height, font_size, 0.0);
209 ctx.text(text, tx, ty, font_size, [0xcc, 0xcc, 0xd4]);
210 }
211 }
212
213 impl Input for Playbar {
214 fn is_dragging(&self) -> bool {
215 self.dragging
216 }
217
218 fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
219 let rect = ectx.rect;
220 match event {
221 Event::MouseButton { button: MouseButton::Left, state, x, y, .. } => match state {
222 ElementState::Pressed => {
223 let b = self.button_rect(rect);
224 if *x >= b.x && *x <= b.x + b.width && *y >= b.y && *y <= b.y + b.height {
225 // The button is the FORWARD transport: playing (either
226 // direction) pauses; paused starts forward. Reverse is
227 // the Down-arrow chord's domain.
228 self.playing = !self.playing;
229 if self.playing {
230 self.reversed = false;
231 }
232 return true;
233 }
234 let t = self.track_rect(rect);
235 // A generous vertical band around the slim track.
236 if *x >= t.x && *x <= t.x + t.width && *y >= rect.y && *y <= rect.y + rect.height {
237 self.dragging = true;
238 self.scrub_to(*x, t);
239 return true;
240 }
241 false
242 }
243 ElementState::Released => std::mem::take(&mut self.dragging),
244 },
245 Event::PointerMove { x, .. } => {
246 if self.dragging {
247 let t = self.track_rect(rect);
248 self.scrub_to(*x, t);
249 true
250 } else {
251 false
252 }
253 }
254 _ => false,
255 }
256 }
257
258 fn tick(&mut self, dt: f32, _rect: Rect) -> bool {
259 if !self.playing {
260 return false;
261 }
262 let dir = if self.reversed { -1.0 } else { 1.0 };
263 self.current_frame += dir * dt * self.fps;
264 let range = (self.end_frame - self.start_frame).max(1.0);
265 if self.current_frame > self.end_frame {
266 self.current_frame = self.start_frame + (self.current_frame - self.start_frame) % range;
267 } else if self.current_frame < self.start_frame {
268 // The reverse wrap, mirroring the forward one: run off the start,
269 // come back in from the end.
270 self.current_frame = self.end_frame - (self.start_frame - self.current_frame) % range;
271 }
272 true
273 }
274 }