1use makepad_widgets::*;
2
3live_design! {
4 import makepad_widgets::base::*;
5 import makepad_widgets::theme_desktop_dark::*;
6 import crate::shared::styles::*;
7
8 ANIMATION_DURATION = 0.65
9 DEFAULT_SWING_TOP = 10.0
10 DEFAULT_SWING_BOTTOM = 3.0
11
12 // 1. Set the width and height to the same value.
13 // 2. Set the radius to half of the width/height.
14 EllipsisDot = <CircleView> {
15 width: 3
16 height: 3
17 draw_bg: {
18 radius: 1.5
19 color: (TYPING_NOTICE_TEXT_COLOR)
20 }
21 }
22
23 TypingAnimation = {{TypingAnimation}} {
24 width: Fit,
25 height: Fit,
26
27 // Add configurable animation parameters
28 swing_amplitude: (DEFAULT_SWING_TOP), // Maximum height of ball swing
29 swing_base: (DEFAULT_SWING_BOTTOM), // Base height of ball swing
30 animation_duration: (ANIMATION_DURATION), // Animation cycle
31
32 flow: Down,
33 align: {x: 0.0, y: 0.5},
34
35 content = <View> {
36 width: Fit,
37 height: Fit,
38 spacing: 2,
39 circle1 = <EllipsisDot> {}
40 circle2 = <EllipsisDot> {}
41 circle3 = <EllipsisDot> {}
42 }
43
44 animator: {
45 circle1 = {
46 default: down,
47 down = {
48 redraw: true,
49 from: {all: Forward {duration: (ANIMATION_DURATION * 0.5)}}
50 ease: InOutQuad
51 apply: {content = { circle1 = { margin: {top: (DEFAULT_SWING_TOP)} }}}
52 }
53 up = {
54 redraw: true,
55 from: {all: Forward {duration: (ANIMATION_DURATION * 0.5)}}
56 ease: InOutQuad
57 apply: {content = { circle1 = { margin: {top: (DEFAULT_SWING_BOTTOM)} }}}
58 }
59 }
60
61 circle2 = {
62 default: down,
63 down = {
64 redraw: true,
65 from: {all: Forward {duration: (ANIMATION_DURATION * 0.5)}}
66 ease: InOutQuad
67 apply: {content = { circle2 = { margin: {top: (DEFAULT_SWING_TOP)} }}}
68 }
69 up = {
70 redraw: true,
71 from: {all: Forward {duration: (ANIMATION_DURATION * 0.5)}}
72 ease: InOutQuad
73 apply: {content = { circle2 = { margin: {top: (DEFAULT_SWING_BOTTOM)} }}}
74 }
75 }
76
77 circle3 = {
78 default: down,
79 down = {
80 redraw: true,
81 from: {all: Forward {duration: (ANIMATION_DURATION * 0.5)}}
82 ease: InOutQuad
83 apply: {content = { circle3 = { margin: {top: (DEFAULT_SWING_TOP)} }}}
84 }
85 up = {
86 redraw: true,
87 from: {all: Forward {duration: (ANIMATION_DURATION * 0.5)}}
88 ease: InOutQuad
89 apply: {content = { circle3 = { margin: {top: (DEFAULT_SWING_BOTTOM)} }}}
90 }
91 }
92 }
93 }
94}
95
96#[derive(Live, Widget)]
97pub struct TypingAnimation {
98 #[deref] view: View,
99 #[animator] animator: Animator,
100
101 #[live(0.65)] animation_duration: f64,
102 #[live(10.0)] swing_amplitude: f64,
103 #[live(3.0)] swing_base: f64,
104
105 #[rust] next_frame: Option<NextFrame>,
106 #[rust] animation_start_time: f64,
107 #[rust] current_animated_dot: CurrentAnimatedDot,
108
109 #[rust] initialized: bool,
110 #[rust] is_animating: bool,
111}
112
113impl LiveHook for TypingAnimation {
114 fn after_apply(&mut self, cx: &mut Cx, apply: &mut Apply, index: usize, nodes: &[LiveNode]) {
115 if !self.initialized {
116 self.initialized = true;
117 }
118 }
119}
120
121#[derive(Copy, Clone, Default)]
122enum CurrentAnimatedDot {
123 #[default]
124 Dot1,
125 Dot2,
126 Dot3,
127}
128impl CurrentAnimatedDot {
129 fn next(&self) -> Self {
130 match self {
131 Self::Dot1 => Self::Dot2,
132 Self::Dot2 => Self::Dot3,
133 Self::Dot3 => Self::Dot1,
134 }
135 }
136}
137
138impl Widget for TypingAnimation {
139 fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
140 if self.initialized && !self.is_animating {
141 self.init(cx);
142 }
143
144 match event {
145 Event::NextFrame(ne) => {
146 if let Some(next_frame) = self.next_frame {
147 if ne.set.contains(&next_frame) {
148 let elapsed = ne.time - self.animation_start_time;
149 if elapsed >= self.animation_duration * 0.5 {
150 self.update_animation(cx, ne.time);
151 }
152 // Continue to request the next frame
153 self.next_frame = Some(cx.new_next_frame());
154 }
155 } else if self.initialized && !self.is_animating {
156 // If the component is initialized but has no animation, restart the animation
157 self.animation_start_time = ne.time;
158 self.next_frame = Some(cx.new_next_frame());
159 self.update_animation(cx, ne.time);
160 self.is_animating = true;
161 }
162 }
163 Event::WindowGeomChange(_) => {
164 self.reset_animation(cx);
165 }
166 _ => {}
167 }
168
169 if self.animator_handle_event(cx, event).must_redraw() {
170 self.redraw(cx);
171 }
172
173 self.view.handle_event(cx, event, scope);
174 }
175
176 fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
177 self.view.draw_walk(cx, scope, walk)
178 }
179}
180
181impl TypingAnimation {
182
183 pub fn init(&mut self, cx: &mut Cx) {
184 self.animation_start_time = cx.seconds_since_app_start();
185 self.next_frame = Some(cx.new_next_frame());
186
187 if let Some(state) = &mut self.animator.state {
188 state.push_live(live!{
189 state: {
190 content: {
191 circle1: { margin: { top: (self.swing_base) }},
192 circle2: { margin: { top: (self.swing_base) }},
193 circle3: { margin: { top: (self.swing_base) }}
194 }
195 }
196 });
197 }
198
199 self.update_animation(cx, self.animation_start_time);
200 self.is_animating = true;
201 }
202
203 fn reset_animation(&mut self, cx: &mut Cx) {
204 self.animation_start_time = cx.seconds_since_app_start();
205 self.next_frame = Some(cx.new_next_frame());
206 self.current_animated_dot = CurrentAnimatedDot::default();
207 self.is_animating = false;
208 }
209 pub fn update_animation(&mut self, cx: &mut Cx, time: f64) {
210 self.current_animated_dot = self.current_animated_dot.next();
211 self.animation_start_time = time;
212
213 match self.current_animated_dot {
214 CurrentAnimatedDot::Dot1 => {
215 let mut up_nodes = LiveNodeVec::new();
216 up_nodes.push_live(live_object!{
217 content: {
218 circle1: {
219 margin: {top: (self.swing_base)}
220 }
221 }
222 });
223
224 let mut down_nodes = LiveNodeVec::new();
225 down_nodes.push_live(live_object!{
226 content: {
227 circle3: {
228 margin: {top: (self.swing_amplitude)}
229 }
230 }
231 });
232
233 if let Some(state) = &mut self.animator.state {
234 state.replace_or_insert_last_node_by_path(
235 0,
236 &[live_id!(circle1).as_field(), live_id!(up).as_field(), live_id!(apply).as_field()],
237 &up_nodes
238 );
239
240 state.replace_or_insert_last_node_by_path(
241 0,
242 &[live_id!(circle3).as_field(), live_id!(down).as_field(), live_id!(apply).as_field()],
243 &down_nodes
244 );
245 }
246
247 self.animator_play(cx, id!(circle1.up));
248 self.animator_play(cx, id!(circle3.down));
249 }
250 CurrentAnimatedDot::Dot2 => {
251 let mut up_nodes = LiveNodeVec::new();
252 up_nodes.push_live(live_object!{
253 content: {
254 circle2: {
255 margin: {top: (self.swing_base)}
256 }
257 }
258 });
259
260 let mut down_nodes = LiveNodeVec::new();
261 down_nodes.push_live(live_object!{
262 content: {
263 circle1: {
264 margin: {top: (self.swing_amplitude)}
265 }
266 }
267 });
268
269 if let Some(state) = &mut self.animator.state {
270 state.replace_or_insert_last_node_by_path(
271 0,
272 &[live_id!(circle2).as_field(), live_id!(up).as_field(), live_id!(apply).as_field()],
273 &up_nodes
274 );
275
276 state.replace_or_insert_last_node_by_path(
277 0,
278 &[live_id!(circle1).as_field(), live_id!(down).as_field(), live_id!(apply).as_field()],
279 &down_nodes
280 );
281 }
282
283 self.animator_play(cx, id!(circle1.down));
284 self.animator_play(cx, id!(circle2.up));
285 }
286 CurrentAnimatedDot::Dot3 => {
287 let mut up_nodes = LiveNodeVec::new();
288 up_nodes.push_live(live_object!{
289 content: {
290 circle3: {
291 margin: {top: (self.swing_base)}
292 }
293 }
294 });
295
296 let mut down_nodes = LiveNodeVec::new();
297 down_nodes.push_live(live_object!{
298 content: {
299 circle2: {
300 margin: {top: (self.swing_amplitude)}
301 }
302 }
303 });
304
305 if let Some(state) = &mut self.animator.state {
306 state.replace_or_insert_last_node_by_path(
307 0,
308 &[live_id!(circle3).as_field(), live_id!(up).as_field(), live_id!(apply).as_field()],
309 &up_nodes
310 );
311
312 state.replace_or_insert_last_node_by_path(
313 0,
314 &[live_id!(circle2).as_field(), live_id!(down).as_field(), live_id!(apply).as_field()],
315 &down_nodes
316 );
317 }
318
319 self.animator_play(cx, id!(circle2.down));
320 self.animator_play(cx, id!(circle3.up));
321 }
322 };
323 }
324
325 pub fn start(&mut self, cx: &mut Cx) {
326 if !self.is_animating {
327 self.initialized = true;
328 self.is_animating = false;
329 self.reset_animation(cx);
330 self.animation_start_time = cx.seconds_since_app_start();
331 self.next_frame = Some(cx.new_next_frame());
332 self.update_animation(cx, self.animation_start_time);
333 }
334 }
335
336 pub fn stop(&mut self, cx: &mut Cx) {
337 if self.is_animating {
338 self.is_animating = false;
339 self.next_frame = None;
340
341 if let Some(state) = &mut self.animator.state {
342 state.push_live(live!{
343 state: {
344 content: {
345 circle1: { margin: { top: (self.swing_base) }},
346 circle2: { margin: { top: (self.swing_base) }},
347 circle3: { margin: { top: (self.swing_base) }}
348 }
349 }
350 });
351 }
352 self.redraw(cx);
353 }
354 }
355
356}
357
358
359
360/// typing_animation.set_swing_parameters(cx, 5.0, 2.0);
361/// typing_animation.set_animation_speed(cx, 0.3);
362///
363///
364/// typing_animation.set_swing_parameters(cx, 15.0, 3.0);
365/// typing_animation.set_animation_speed(cx, 1.0);
366impl TypingAnimationRef {
367
368 pub fn set_swing_parameters(&self, cx: &mut Cx, amplitude: f64, base: f64) {
369 if let Some(mut inner) = self.borrow_mut() {
370 inner.swing_amplitude = amplitude;
371 inner.swing_base = base;
372
373 if inner.next_frame.is_some() {
374 let time = cx.seconds_since_app_start();
375 inner.update_animation(cx, time);
376 }
377 }
378 }
379
380 pub fn set_animation_speed(&self, cx: &mut Cx, duration: f64) {
381 if let Some(mut inner) = self.borrow_mut() {
382 inner.animation_duration = duration;
383
384 if inner.next_frame.is_some() {
385 let time = cx.seconds_since_app_start();
386 inner.update_animation(cx, time);
387 }
388 }
389 }
390
391 pub fn start(&self, cx: &mut Cx) {
392 // self.set_swing_parameters(cx, 5.0, 2.0);
393 self.set_animation_speed(cx, 0.3);
394 if let Some(mut inner) = self.borrow_mut() {
395 inner.start(cx);
396 }
397 }
398
399 pub fn stop(&self, cx: &mut Cx) {
400 if let Some(mut inner) = self.borrow_mut() {
401 inner.stop(cx);
402 }
403 }
404}