Skip to content

Commit 27d4615

Browse files
committed
feat: HintBox fade out animation
1 parent 66b004a commit 27d4615

3 files changed

Lines changed: 53 additions & 14 deletions

File tree

src/app_engine/lifecycle.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ impl AppEngine {
184184
.any(|p_f| p_f.intersect(&frame).is_some())
185185
{
186186
hb.disabled = true;
187-
hb.set_visible(false);
187+
hb.fade_out();
188188
}
189189
}
190190
}

src/user_interface/animation.rs

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ use objc2::{
55
};
66
use objc2_core_foundation::{CFRetained, CGPoint, CGRect, CGSize};
77
use objc2_core_graphics::{CGColor, CGMutablePath, CGPath};
8-
use objc2_foundation::{NSArray, NSNumber, NSString};
8+
use objc2_foundation::{NSArray, NSNumber, ns_string};
99
use objc2_quartz_core::{
10-
CAAnimation, CAAnimationGroup, CABasicAnimation, CAGradientLayer, CAMediaTiming,
10+
CAAnimation, CAAnimationGroup, CABasicAnimation, CAGradientLayer, CALayer, CAMediaTiming,
1111
CAMediaTimingFunction, CAShapeLayer, CATransaction, kCAMediaTimingFunctionEaseOut,
1212
};
1313

14-
use crate::user_interface::{UIDrawer, calibrated_origin};
14+
use crate::user_interface::{HintBox, UIDrawer, calibrated_origin};
1515

1616
const RIPPLE_DURATION: f64 = 0.4;
1717
const RIPPLE_INIT_RADIUS: f64 = 5.0;
@@ -22,6 +22,8 @@ const TRAIL_DURATION: f64 = 0.5;
2222
const CURSOR_OFFSET_X: f64 = 9.0;
2323
const CURSOR_OFFSET_Y: f64 = 15.0;
2424

25+
const HINT_FADE_OUT_DURATION: f64 = 1.0;
26+
2527
impl UIDrawer {
2628
/// Triggers a ripple animation at the given (x, y) coordinates inside a parent CALayer.
2729
pub fn draw_ripple(&self, x: f64, y: f64, color: &CFRetained<CGColor>) {
@@ -65,15 +67,13 @@ impl UIDrawer {
6567
CATransaction::setCompletionBlock(Some(&completion_block));
6668

6769
// Create the Scale Animation (Expanding)
68-
let scale_anim = CABasicAnimation::animationWithKeyPath(Some(&NSString::from_str(
69-
"transform.scale",
70-
)));
70+
let scale_anim =
71+
CABasicAnimation::animationWithKeyPath(Some(ns_string!("transform.scale")));
7172
scale_anim.setFromValue(Some(&NSNumber::new_f64(1.0)));
7273
scale_anim.setToValue(Some(&NSNumber::new_f64(RIPPLE_SCALE_FACTOR))); // Grows 6x the initial radius
7374

7475
// Create the Opacity Animation (Fading Out)
75-
let fade_anim =
76-
CABasicAnimation::animationWithKeyPath(Some(&NSString::from_str("opacity")));
76+
let fade_anim = CABasicAnimation::animationWithKeyPath(Some(ns_string!("opacity")));
7777
fade_anim.setFromValue(Some(&NSNumber::new_f64(1.0)));
7878
fade_anim.setToValue(Some(&NSNumber::new_f64(0.0)));
7979

@@ -96,7 +96,7 @@ impl UIDrawer {
9696

9797
// Bind animation to layer and add layer to window/view hierarchy
9898
// The key "ripple" can be anything unique
99-
ripple_layer.addAnimation_forKey(&anim_group, Some(&NSString::from_str("ripple")));
99+
ripple_layer.addAnimation_forKey(&anim_group, Some(ns_string!("ripple")));
100100

101101
CATransaction::commit();
102102
}
@@ -224,7 +224,7 @@ impl UIDrawer {
224224
// At t=1, locations are [1.0, 2.0], meaning the entire range [0, 1] is transparent.
225225
// This results in the "fade front" moving from the start point to the end point.
226226
let loc_anim =
227-
CABasicAnimation::animationWithKeyPath(Some(&NSString::from_str("locations")));
227+
CABasicAnimation::animationWithKeyPath(Some(ns_string!("locations")));
228228
let loc_start = NSArray::from_retained_slice(&[
229229
NSNumber::new_f64(-0.2),
230230
NSNumber::new_f64(0.0),
@@ -240,7 +240,7 @@ impl UIDrawer {
240240
CAMediaTimingFunction::functionWithName(kCAMediaTimingFunctionEaseOut);
241241
loc_anim.setTimingFunction(Some(&timing_function));
242242

243-
mask_layer.addAnimation_forKey(&loc_anim, Some(&NSString::from_str("locations")));
243+
mask_layer.addAnimation_forKey(&loc_anim, Some(ns_string!("locations")));
244244
// Set the model's locations to the final value to prevent flickering after animation
245245
mask_layer.setLocations(Some(&loc_end));
246246

@@ -249,3 +249,42 @@ impl UIDrawer {
249249
});
250250
}
251251
}
252+
253+
impl HintBox {
254+
/// Fades the hint box out, then hides it.
255+
pub fn fade_out(&self) {
256+
let layers: Vec<Retained<CALayer>> = std::iter::once(self.box_layer.clone())
257+
.chain(self.frame_layer.iter().cloned())
258+
.collect();
259+
260+
for layer in layers {
261+
// Set the model opacity to 0 before the animation so the layer
262+
// stays hidden after the animation is removed.
263+
layer.setOpacity(0.0);
264+
265+
CATransaction::begin();
266+
267+
let layer_clone = layer.clone();
268+
let completion = RcBlock::new(move || {
269+
layer_clone.setHidden(true);
270+
// Restore opacity so the layer can be shown again later.
271+
layer_clone.setOpacity(1.0);
272+
});
273+
unsafe {
274+
CATransaction::setCompletionBlock(Some(&completion));
275+
276+
let anim = CABasicAnimation::animationWithKeyPath(Some(ns_string!("opacity")));
277+
anim.setFromValue(Some(&NSNumber::new_f64(1.0)));
278+
anim.setToValue(Some(&NSNumber::new_f64(0.0)));
279+
anim.setDuration(HINT_FADE_OUT_DURATION);
280+
anim.setRemovedOnCompletion(true);
281+
282+
let timing = CAMediaTimingFunction::functionWithName(kCAMediaTimingFunctionEaseOut);
283+
anim.setTimingFunction(Some(&timing));
284+
285+
layer.addAnimation_forKey(&anim, Some(ns_string!("fade_out")));
286+
}
287+
CATransaction::commit();
288+
}
289+
}
290+
}

src/user_interface/hint_box.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ pub struct HintBox {
4545
frame: Option<Frame>,
4646
color: Option<CFRetained<CGColor>>,
4747
text_layer: Retained<CATextLayer>,
48-
box_layer: Retained<CALayer>,
48+
pub(super) box_layer: Retained<CALayer>,
4949
tri_layer: Retained<CAShapeLayer>,
50-
frame_layer: Option<Retained<CALayer>>,
50+
pub(super) frame_layer: Option<Retained<CALayer>>,
5151
}
5252

5353
impl HintBox {

0 commit comments

Comments
 (0)