1use std::{
5 cell::{Cell, RefCell},
6 rc::Rc,
7};
8
9use cranpose_animation::{Animatable, AnimationType, Easing, spring, tween};
10use cranpose_core::{RuntimeHandle, State, with_current_composer};
11use cranpose_foundation::VelocityTracker1D;
12use cranpose_macros::composable;
13use cranpose_ui::{Modifier, MutableInteractionSource};
14use cranpose_ui_graphics::GraphicsLayer;
15
16use crate::dynamics::{LiquidDynamics, LiquidPose};
17
18const FLUID_RELAX_MS: u64 = 420;
19const VISUAL_HANDOFF_TOLERANCE_IN_ITEMS: f32 = 0.12;
20
21pub(crate) fn liquid_visual_index(
22 selected: usize,
23 lens_position: f32,
24 item_width: f32,
25 count: usize,
26 lens_owns_selection: bool,
27) -> usize {
28 if count == 0 {
29 return 0;
30 }
31 let selected = selected.min(count - 1);
32 if !lens_owns_selection || !lens_position.is_finite() || item_width <= f32::EPSILON {
33 return selected;
34 }
35 ((lens_position + item_width * 0.5) / item_width)
36 .floor()
37 .clamp(0.0, count.saturating_sub(1) as f32) as usize
38}
39
40pub(crate) fn liquid_axis_owns_visual_selection(
41 direct: bool,
42 lens_position: f32,
43 state_position: f32,
44 item_width: f32,
45) -> bool {
46 direct
47 || (lens_position - state_position).abs()
48 > item_width.max(f32::EPSILON) * VISUAL_HANDOFF_TOLERANCE_IN_ITEMS
49}
50
51pub struct LiquidMotion;
54
55impl LiquidMotion {
56 pub fn snappy() -> AnimationType {
58 spring(0.85, 900.0)
59 }
60
61 pub fn bouncy() -> AnimationType {
63 spring(0.55, 500.0)
64 }
65
66 pub fn smooth() -> AnimationType {
68 spring(1.0, 400.0)
69 }
70
71 pub fn blob_leading() -> AnimationType {
73 spring(0.8, 900.0)
74 }
75
76 pub fn blob_trailing() -> AnimationType {
79 spring(0.9, 380.0)
80 }
81
82 pub fn glide() -> AnimationType {
87 spring(1.0, 500.0)
88 }
89}
90
91pub(crate) struct LiquidDragAxis {
92 animation: RefCell<Animatable<f32>>,
93 follow: Option<AnimationType>,
94 pointer: Cell<Option<f32>>,
95 velocity: RefCell<VelocityTracker1D>,
96 runtime: RuntimeHandle,
97 last_sample_ms: Cell<Option<i64>>,
98 dynamics: LiquidDynamics,
99 fluid_clock: RefCell<Animatable<f32>>,
100}
101
102impl LiquidDragAxis {
103 fn new(initial: f32, runtime: RuntimeHandle, follow: Option<AnimationType>) -> Self {
104 Self {
105 follow,
106 animation: RefCell::new(Animatable::new(initial, runtime.clone())),
107 pointer: Cell::new(None),
108 velocity: RefCell::new(VelocityTracker1D::new()),
109 dynamics: LiquidDynamics::new(runtime.clone()),
110 fluid_clock: RefCell::new(Animatable::new(1.0, runtime.clone())),
111 runtime,
112 last_sample_ms: Cell::new(None),
113 }
114 }
115
116 fn arm_fluid_frames(&self) {
117 let mut clock = self.fluid_clock.borrow_mut();
118 clock.snapTo(0.0);
119 clock.animateTo(1.0, tween(FLUID_RELAX_MS, Easing::LinearEasing));
120 }
121
122 fn sample_time_ms(&self, event_time_ms: Option<i64>) -> i64 {
123 let candidate = event_time_ms
124 .or_else(|| {
125 self.runtime
126 .last_frame_time_nanos()
127 .map(|nanos| (nanos / 1_000_000) as i64)
128 })
129 .unwrap_or_else(|| self.last_sample_ms.get().unwrap_or(0) + 16);
130 let monotonic = self
131 .last_sample_ms
132 .get()
133 .map_or(candidate, |last| candidate.max(last + 1));
134 self.last_sample_ms.set(Some(monotonic));
135 monotonic
136 }
137
138 pub(crate) fn begin(&self, position: f32, event_time_ms: Option<i64>) {
139 let time_ms = self.sample_time_ms(event_time_ms);
140 let mut velocity = self.velocity.borrow_mut();
141 velocity.reset();
142 velocity.add_data_point(time_ms, position);
143 self.pointer.set(Some(position));
144 self.animation.borrow_mut().snapTo(position);
145 self.dynamics.anchor_pointer((position, 0.0));
146 self.arm_fluid_frames();
147 }
148
149 pub(crate) fn move_to(&self, position: f32, event_time_ms: Option<i64>) {
150 self.move_to_at(
151 position,
152 event_time_ms,
153 self.runtime.last_frame_time_nanos(),
154 );
155 }
156
157 fn move_to_at(
158 &self,
159 position: f32,
160 event_time_ms: Option<i64>,
161 animation_time_nanos: Option<u64>,
162 ) {
163 if self.pointer.get().is_none() {
164 return;
165 }
166 let previous_time_ms = self.last_sample_ms.get();
167 let time_ms = self.sample_time_ms(event_time_ms);
168 self.velocity.borrow_mut().add_data_point(time_ms, position);
169 self.pointer.set(Some(position));
170 if let Some(animation) = self.follow {
171 let mut value = self.animation.borrow_mut();
172 if let Some(time) = animation_time_nanos {
173 value.animate_to_at(position, animation, time);
174 } else {
175 value.animateTo(position, animation);
176 }
177 } else {
178 self.animation.borrow_mut().snapTo(position);
179 }
180 if let Some(previous_time_ms) = previous_time_ms.filter(|_| self.follow.is_none()) {
181 let dt = (time_ms - previous_time_ms).max(1) as f32 / 1000.0;
182 self.dynamics.advance_pointer((position, 0.0), dt);
183 }
184 self.arm_fluid_frames();
185 }
186
187 pub(crate) fn release_to(
188 &self,
189 target: f32,
190 event_time_ms: Option<i64>,
191 animation: AnimationType,
192 ) {
193 self.release_to_at(
194 target,
195 event_time_ms,
196 animation,
197 self.runtime.last_frame_time_nanos(),
198 );
199 }
200
201 fn release_to_at(
202 &self,
203 target: f32,
204 event_time_ms: Option<i64>,
205 animation: AnimationType,
206 animation_time_nanos: Option<u64>,
207 ) {
208 let Some(position) = self.pointer.take() else {
209 self.settle_to_at(target, animation, animation_time_nanos);
210 return;
211 };
212 let time_ms = self.sample_time_ms(event_time_ms);
213 self.velocity.borrow_mut().add_data_point(time_ms, position);
214 let release_velocity = if self.follow.is_some() {
215 self.animation.borrow().velocity()
216 } else {
217 self.velocity.borrow().calculate_velocity_with_max(8_000.0)
218 };
219 self.dynamics.release_pointer();
220 let mut value = self.animation.borrow_mut();
221 if let Some(time) = animation_time_nanos {
222 if self.follow.is_some() {
223 value.animate_to_at(target, animation, time);
224 } else {
225 value.animate_to_with_velocity_at(target, release_velocity, animation, time);
226 }
227 } else {
228 value.animate_to_with_velocity(target, release_velocity, animation);
229 }
230 }
231
232 pub(crate) fn finish_at(&self, position: f32, event_time_ms: Option<i64>) {
233 let Some(_) = self.pointer.get() else {
234 self.animation.borrow_mut().snapTo(position);
235 return;
236 };
237 let previous_time_ms = self.last_sample_ms.get();
238 let time_ms = self.sample_time_ms(event_time_ms);
239 self.velocity.borrow_mut().add_data_point(time_ms, position);
240 if let Some(previous_time_ms) = previous_time_ms {
241 let dt = (time_ms - previous_time_ms).max(1) as f32 / 1000.0;
242 self.dynamics.advance_pointer((position, 0.0), dt);
243 }
244 self.pointer.set(None);
245 self.animation.borrow_mut().snapTo(position);
246 self.dynamics.release_pointer();
247 self.arm_fluid_frames();
248 }
249
250 pub(crate) fn settle_to(&self, target: f32, animation: AnimationType) {
251 self.settle_to_at(target, animation, self.runtime.last_frame_time_nanos());
252 }
253
254 fn settle_to_at(&self, target: f32, animation: AnimationType, time: Option<u64>) {
255 if self.pointer.get().is_some() {
256 return;
257 }
258 let mut value = self.animation.borrow_mut();
259 if (value.target() - target).abs() > f32::EPSILON {
260 if let Some(time) = time {
261 value.animate_to_at(target, animation, time);
262 } else {
263 value.animateTo(target, animation);
264 }
265 }
266 }
267
268 pub(crate) fn value(&self) -> f32 {
269 let _ = self.fluid_clock.borrow().state().value();
270 if self.follow.is_some() {
271 self.animation.borrow().state().value()
272 } else {
273 self.pointer
274 .get()
275 .unwrap_or_else(|| self.animation.borrow().state().value())
276 }
277 }
278
279 pub(crate) fn liquid_pose(&self) -> LiquidPose {
280 self.dynamics.update_pointer((self.value(), 0.0))
281 }
282
283 pub(crate) fn is_dragging(&self) -> bool {
284 self.pointer.get().is_some()
285 }
286}
287
288#[composable]
289pub(crate) fn remember_liquid_drag_axis(initial: f32) -> Rc<LiquidDragAxis> {
290 remember_liquid_axis(initial, None)
291}
292
293#[composable]
294pub(crate) fn remember_liquid_follow_axis(
295 initial: f32,
296 animation: AnimationType,
297) -> Rc<LiquidDragAxis> {
298 remember_liquid_axis(initial, Some(animation))
299}
300
301#[composable]
302fn remember_liquid_axis(initial: f32, follow: Option<AnimationType>) -> Rc<LiquidDragAxis> {
303 with_current_composer(|composer| {
304 let runtime = composer.runtime_handle();
305 composer
306 .remember(move || Rc::new(LiquidDragAxis::new(initial, runtime, follow)))
307 .with(Rc::clone)
308 })
309}
310
311pub(crate) struct LiquidLensGesture {
312 pub axis: Rc<LiquidDragAxis>,
313 pub cell_width: f32,
314 pub cell_offset: f32,
315 pub count: usize,
316 pub tap_slop: f32,
317 pub drag_left: Rc<dyn Fn(f32) -> f32>,
318 pub rest_left: Rc<dyn Fn(usize) -> f32>,
319 pub selected: usize,
320 pub on_pressed: Rc<dyn Fn(bool, Option<u64>)>,
321 pub on_touch: Rc<dyn Fn(f32, f32)>,
322 pub on_select: Rc<dyn Fn(usize)>,
323}
324
325impl LiquidLensGesture {
326 fn commit_index(&self, position: f32) -> usize {
327 (((position - self.cell_offset) / self.cell_width.max(1.0)).floor() as isize)
328 .clamp(0, self.count.saturating_sub(1) as isize) as usize
329 }
330}
331
332pub(crate) async fn liquid_lens_gesture(
333 scope: cranpose_ui::PointerInputScope,
334 gesture: LiquidLensGesture,
335) {
336 use cranpose_foundation::{PointerEventKind, PointerId};
337 use cranpose_services::{HapticFeedback, default_haptics};
338
339 scope
340 .await_pointer_event_scope(|await_scope| async move {
341 let mut down_x = 0.0f32;
342 let mut moved = false;
343 let mut active_pointer = Option::<PointerId>::None;
344 loop {
345 let event = await_scope.await_pointer_event().await;
346 match event.kind {
347 PointerEventKind::Down if active_pointer.is_none() => {
348 active_pointer = Some(event.id);
349 down_x = event.position.x;
350 moved = false;
351 gesture.axis.settle_to_at(
352 (gesture.rest_left)(gesture.commit_index(down_x)),
353 LiquidMotion::glide(),
354 event.animation_time_nanos,
355 );
356 (gesture.on_pressed)(true, event.animation_time_nanos);
357 (gesture.on_touch)(event.position.x, event.position.y);
358 default_haptics().perform(HapticFeedback::Selection);
359 event.consume();
360 }
361 PointerEventKind::Move if active_pointer == Some(event.id) => {
362 moved |= (event.position.x - down_x).abs() > gesture.tap_slop;
363 if moved {
364 let target = (gesture.drag_left)(event.position.x);
365 if gesture.axis.is_dragging() {
366 gesture.axis.move_to_at(
367 target,
368 event.time_ms,
369 event.animation_time_nanos,
370 );
371 } else if (gesture.axis.value() - target).abs()
372 <= gesture.cell_width * 0.6
373 {
374 gesture.axis.begin(gesture.axis.value(), event.time_ms);
375 gesture.axis.move_to_at(
376 target,
377 event.time_ms,
378 event.animation_time_nanos,
379 );
380 } else {
381 gesture.axis.settle_to_at(
382 target,
383 LiquidMotion::glide(),
384 event.animation_time_nanos,
385 );
386 }
387 }
388 (gesture.on_touch)(event.position.x, event.position.y);
389 event.consume();
390 }
391 PointerEventKind::Up if active_pointer == Some(event.id) => {
392 active_pointer = None;
393 (gesture.on_pressed)(false, event.animation_time_nanos);
394 let commit_x = if moved { event.position.x } else { down_x };
395 let index = gesture.commit_index(commit_x);
396 gesture.axis.release_to_at(
397 (gesture.rest_left)(index),
398 event.time_ms,
399 LiquidMotion::glide(),
400 event.animation_time_nanos,
401 );
402 default_haptics().perform(HapticFeedback::ImpactLight);
403 (gesture.on_select)(index);
404 event.consume();
405 }
406 PointerEventKind::Cancel if active_pointer == Some(event.id) => {
407 active_pointer = None;
408 (gesture.on_pressed)(false, event.animation_time_nanos);
409 gesture.axis.release_to_at(
410 (gesture.rest_left)(gesture.selected),
411 event.time_ms,
412 LiquidMotion::glide(),
413 event.animation_time_nanos,
414 );
415 event.consume();
416 }
417 _ => {}
418 }
419 }
420 })
421 .await;
422}
423
424#[composable]
433pub fn liquid_press_scale(
434 modifier: Modifier,
435 interaction_source: MutableInteractionSource,
436 pressed_scale: f32,
437) -> (Modifier, State<bool>, State<f32>) {
438 let pressed = interaction_source.collectIsPressedAsState();
439 let scale = cranpose_animation::animateFloatAsState(
440 if pressed.get() {
441 pressed_scale.max(1.0)
442 } else {
443 1.0
444 },
445 LiquidMotion::snappy(),
446 "liquid-press-scale",
447 );
448 let content_alpha = cranpose_animation::animateFloatAsState(
449 if pressed.get() { 0.35 } else { 1.0 },
450 LiquidMotion::smooth(),
451 "liquid-press-content",
452 );
453 let modifier = modifier.graphics_layer(move || {
454 let scale = scale.get();
455 GraphicsLayer {
456 scale_x: scale,
457 scale_y: scale,
458 ..Default::default()
459 }
460 });
461 (modifier, pressed, content_alpha)
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 fn axis(initial: f32) -> (cranpose_core::Runtime, LiquidDragAxis) {
469 let runtime =
470 cranpose_core::Runtime::new(std::sync::Arc::new(cranpose_core::DefaultScheduler));
471 let axis = LiquidDragAxis::new(initial, runtime.handle(), None);
472 (runtime, axis)
473 }
474
475 fn native_trace_error(follow: Option<AnimationType>) -> f32 {
476 let runtime =
477 cranpose_core::Runtime::new(std::sync::Arc::new(cranpose_core::DefaultScheduler));
478 let mut axis = LiquidDragAxis::new(0.0, runtime.handle(), follow);
479 let mut origin = 1_000_000_000u64;
480 let mut squared_error = 0.0;
481 let mut count = 0;
482 for line in include_str!("../tests/fixtures/native_tab_drag.csv").lines() {
483 let fields: Vec<_> = line.split(',').collect();
484 let time: f64 = fields[1].parse().expect("trace time");
485 let position: f32 = fields[2].parse().expect("trace position");
486 if fields[0] == "start" {
487 origin += 10_000_000_000;
488 axis = LiquidDragAxis::new(position, runtime.handle(), follow);
489 runtime.handle().drain_frame_callbacks(origin);
490 axis.begin(position, Some((origin / 1_000_000) as i64));
491 continue;
492 }
493 let nanos = origin + (time * 1e9) as u64;
494 runtime.handle().drain_frame_callbacks(nanos);
495 if fields[0] == "input" {
496 axis.move_to(position, Some((nanos / 1_000_000) as i64));
497 } else {
498 squared_error += (axis.value() - position).powi(2);
499 count += 1;
500 }
501 }
502 assert!(count > 200);
503 (squared_error / count as f32).sqrt()
504 }
505
506 #[test]
507 fn tab_follow_matches_both_native_drag_velocities_and_held_settling() {
508 let following = native_trace_error(Some(spring(0.85, 650.0)));
509 let direct = native_trace_error(None);
510 assert!(following < 3.0, "native trace RMS: {following}");
511 assert!(direct > 15.0, "direct-input counterexample RMS: {direct}");
512 }
513
514 #[test]
515 fn releasing_a_following_lens_preserves_its_rendered_velocity() {
516 let runtime =
517 cranpose_core::Runtime::new(std::sync::Arc::new(cranpose_core::DefaultScheduler));
518 let axis = LiquidDragAxis::new(0.0, runtime.handle(), Some(spring(0.85, 650.0)));
519 runtime.handle().drain_frame_callbacks(1_000_000_000);
520 axis.begin(0.0, Some(1000));
521 axis.move_to(100.0, Some(1016));
522 runtime.handle().drain_frame_callbacks(1_016_000_000);
523 runtime.handle().drain_frame_callbacks(1_032_000_000);
524 let before = axis.animation.borrow().velocity();
525 assert!(before > 0.0);
526 axis.release_to(100.0, Some(1200), LiquidMotion::glide());
527 assert_eq!(axis.animation.borrow().velocity(), before);
528 assert!(!axis.is_dragging());
529 }
530
531 #[test]
532 fn pointer_samples_are_the_visual_coordinate_without_a_chase() {
533 let (_runtime, axis) = axis(10.0);
534 axis.begin(20.0, Some(0));
535 assert_eq!(axis.value(), 20.0);
536 axis.move_to(180.0, Some(16));
537 assert_eq!(axis.value(), 180.0);
538 }
539
540 #[test]
541 fn direct_manipulation_owns_visual_selection_until_the_lens_reaches_state() {
542 assert_eq!(liquid_visual_index(2, 0.0, 78.0, 4, false), 2);
543 assert_eq!(liquid_visual_index(0, 2.0 * 78.0, 78.0, 4, true), 2);
544 assert_eq!(liquid_visual_index(0, 1.6 * 78.0, 78.0, 4, true), 2);
545 assert_eq!(liquid_visual_index(0, 1.4 * 78.0, 78.0, 4, true), 1);
546 assert_eq!(liquid_visual_index(0, 2.71 * 78.0, 78.0, 4, true), 3);
547 assert_eq!(liquid_visual_index(0, 3.0 * 78.0, 78.0, 4, true), 3);
548 assert_eq!(liquid_visual_index(0, 99.0 * 78.0, 78.0, 4, true), 3);
549 assert_eq!(liquid_visual_index(9, f32::NAN, 78.0, 4, true), 3);
550 assert_eq!(liquid_visual_index(0, 78.0, 0.0, 4, true), 0);
551
552 assert!(liquid_axis_owns_visual_selection(true, 0.0, 0.0, 78.0));
553 assert!(liquid_axis_owns_visual_selection(false, 78.0, 0.0, 78.0));
554 assert!(!liquid_axis_owns_visual_selection(
555 false,
556 78.0 * 0.05,
557 0.0,
558 78.0,
559 ));
560 }
561
562 #[test]
563 fn pointer_sample_excites_the_incompressible_pose_before_render() {
564 let (_runtime, axis) = axis(0.0);
565 axis.begin(0.0, Some(0));
566 axis.move_to(14.0, Some(16));
567 let pose = axis.liquid_pose();
568 let deformation = (pose.stretch - 1.0).abs();
569 assert!(
570 (0.08..=0.12).contains(&deformation),
571 "the direct-input frame must deform visibly without treating one sample as extreme acceleration: {pose:?}"
572 );
573 assert!((pose.stretch * pose.ortho - 1.0).abs() < 1e-4);
574 assert_eq!(axis.value(), 14.0);
575 }
576
577 #[test]
578 fn render_without_a_new_pointer_sample_preserves_velocity_continuity() {
579 let (_runtime, axis) = axis(0.0);
580 axis.runtime.drain_frame_callbacks(1_000_000);
581 axis.begin(0.0, Some(0));
582 axis.move_to(14.0, Some(16));
583 let sampled = axis.liquid_pose();
584
585 axis.runtime.drain_frame_callbacks(17_000_000);
586 let next_frame = axis.liquid_pose();
587
588 assert!(
589 (next_frame.stretch - sampled.stretch).abs() < 0.08,
590 "a render frame without input must not synthesize a brake impulse: {sampled:?} -> {next_frame:?}"
591 );
592 assert!((next_frame.stretch * next_frame.ortho - 1.0).abs() < 1e-4);
593 }
594
595 #[test]
596 fn controlled_retargets_wait_until_direct_manipulation_ends() {
597 let (_runtime, axis) = axis(10.0);
598 axis.begin(40.0, Some(0));
599 axis.settle_to(90.0, LiquidMotion::snappy());
600 assert_eq!(axis.value(), 40.0);
601 axis.release_to(90.0, Some(16), LiquidMotion::snappy());
602 assert!(!axis.is_dragging());
603 assert_eq!(axis.animation.borrow().target(), 90.0);
604 }
605
606 #[test]
607 fn continuous_release_stops_translation_without_erasing_fluid_velocity() {
608 let (_runtime, axis) = axis(0.0);
609 axis.runtime.drain_frame_callbacks(1_000_000);
610 axis.begin(0.0, Some(0));
611 axis.move_to(80.0, Some(16));
612 let moving = axis.liquid_pose();
613
614 axis.finish_at(80.0, Some(17));
615 assert!(!axis.is_dragging());
616 assert_eq!(axis.value(), 80.0);
617 assert!(moving.speed > 0.0);
618
619 let mut relaxed = moving;
620 for frame in 2..=12 {
621 axis.runtime.drain_frame_callbacks(frame * 17_000_000);
622 assert_eq!(axis.value(), 80.0, "frame {frame} backtracked");
623 relaxed = axis.liquid_pose();
624 }
625 assert!(
626 relaxed.speed < moving.speed,
627 "shape velocity must relax even though translation stops"
628 );
629 }
630
631 #[test]
632 fn released_flight_is_critically_damped() {
633 let AnimationType::Spring(spec) = LiquidMotion::glide() else {
634 panic!("released flight must use a spring");
635 };
636 assert_eq!(spec.damping_ratio, 1.0);
637 assert_eq!(spec.stiffness, 500.0);
638 }
639
640 fn spring_of(animation: AnimationType) -> cranpose_animation::SpringSpec {
641 match animation {
642 AnimationType::Spring(spec) => spec,
643 other => panic!("expected a spring, got {other:?}"),
644 }
645 }
646
647 #[test]
648 fn a_stretching_blob_runs_its_leading_edge_ahead_of_its_trailing_edge() {
649 let leading = spring_of(LiquidMotion::blob_leading());
650 let trailing = spring_of(LiquidMotion::blob_trailing());
651 assert!(
652 leading.stiffness > trailing.stiffness,
653 "the edge that runs ahead has to be the stiffer one: \
654 leading {} vs trailing {}",
655 leading.stiffness,
656 trailing.stiffness
657 );
658 assert!(
659 leading.damping_ratio < 1.0 && trailing.damping_ratio < 1.0,
660 "both edges stay under-damped so the droplet keeps its elongation"
661 );
662 }
663
664 #[test]
665 fn every_named_motion_is_a_settled_spring() {
666 for (name, animation) in [
667 ("snappy", LiquidMotion::snappy()),
668 ("bouncy", LiquidMotion::bouncy()),
669 ("smooth", LiquidMotion::smooth()),
670 ("blob_leading", LiquidMotion::blob_leading()),
671 ("blob_trailing", LiquidMotion::blob_trailing()),
672 ("glide", LiquidMotion::glide()),
673 ] {
674 let spec = spring_of(animation);
675 assert!(spec.stiffness > 0.0, "{name} needs a stiffness");
676 assert!(spec.damping_ratio > 0.0, "{name} needs damping");
677 assert_eq!(spec.delay_millis, 0, "{name} starts on the frame it is set");
678 }
679 }
680}