pub struct Tween<T: Lerp> { /* private fields */ }Expand description
A finite eased interpolation between two values on a caller-supplied clock.
A tween never owns time: sampling and retargeting take now, so one
value drives retained repaints and immediate-mode paints alike.
Tween::retarget restarts from the current sample, so interrupting a
running transition never jumps.
§Example
use std::time::Duration;
use omp_tui::anim::{Easing, Tween};
let mut fade = Tween::settled(0.0f32);
fade.retarget(Duration::ZERO, 1.0, Duration::from_millis(100), Easing::Linear);
assert_eq!(fade.sample(Duration::from_millis(50)), 0.5);
assert!(fade.is_settled(Duration::from_millis(100)));Implementations§
Source§impl<T: Lerp> Tween<T>
impl<T: Lerp> Tween<T>
Sourcepub const fn settled(value: T) -> Self
pub const fn settled(value: T) -> Self
A tween already settled at value.
Examples found in repository?
examples/chat/welcome.rs (line 183)
167 pub fn new(charset: Charset) -> Self {
168 Self {
169 charset,
170 frame: Frame::new(Size::new(0, 0)),
171 title: fmts!(" {} omp v{} ", charset.icon(Icon::Omp), env!("CARGO_PKG_VERSION")),
172 camera: (0.0, 0.0),
173 camera_target: (0.0, 0.0),
174 last_elapsed: 0.0,
175 logo_origin: (0, 0),
176 logo: [[None; LOGO_COLS]; LOGO_ROWS],
177 logo_at: None,
178 backdrop_frame: Frame::new(Size::new(0, 0)),
179 backdrop_at: None,
180 backdrop: Eclipse::default(),
181 surface: Surface::new(),
182 pointer: None,
183 hover: Tween::settled(0.0),
184 }
185 }More examples
examples/chat/demo.rs (line 762)
752 pub fn new(ctx: &UiContext) -> Self {
753 let editor = Rc::new(RefCell::new({
754 let mut editor = Editor::new(EditorOptions::default());
755 editor.set_completion(Box::new(SlashCommands::new(demo_commands())));
756 editor
757 }));
758 let edit_outcome = Rc::new(RefCell::new(None));
759 let work = Rc::new(RefCell::new(WorkState {
760 working: true,
761 since: Duration::ZERO,
762 fade: Tween::settled(GREEN),
763 }));
764 let model = Rc::new(RefCell::new(Str::new_static("Fable 5++")));
765 let pane = EditorPane::new()
766 .input(DemoInput::new(Rc::clone(&editor), Rc::clone(&edit_outcome)))
767 .status(DemoStatus::new(Rc::clone(&work), Rc::clone(&model), ctx.charset));
768 let attachments = pane.attachments();
769 let editor_ui = Ui::from_root(pane, 0, ctx.clone());
770 Self {
771 started_at: Instant::now(),
772 ctx: ctx.clone(),
773 cancel_hint: ctx.charset.icon(Icon::Cancellable),
774 editor_ui,
775 editor,
776 edit_outcome,
777 work,
778 last_working: true,
779 model,
780 attachments,
781 transcript: vec![Entry::Command],
782 drawn_entries: 0,
783 transcript_rows: 0,
784 appended_messages: 0,
785 emitted_shards: 0,
786 last_viewport: Size::new(0, 0),
787 height_floor: 0,
788 frame: Frame::new(Size::new(0, 0)),
789 live_panel: None,
790 live_rows: std::array::from_fn(|_| LiveRowCache::new()),
791 live_label_scratch: StrMut::with_capacity(40),
792 right_inset: 0,
793 switch_requested: false,
794 }
795 }Sourcepub fn sample(&self, now: Duration) -> T
pub fn sample(&self, now: Duration) -> T
The value at time now.
Examples found in repository?
examples/chat/demo.rs (line 589)
576 fn brand_segment(&self, now: Duration) -> Segment {
577 let work = self.work.borrow();
578 let brand = if work.working {
579 fmts!(
580 "{} {}",
581 self.charset.spinner().at(now),
582 elapsed_label(now.saturating_sub(work.since))
583 )
584 } else {
585 fmts!("{} omp", self.charset.icon(Icon::Omp))
586 };
587 Segment::new()
588 .label(brand)
589 .with(Prop::Fg, work.fade.sample(now))
590 }More examples
examples/chat/welcome.rs (line 250)
203 pub fn render(&mut self, viewport: Size, elapsed: Duration) -> &Frame {
204 if self.frame.size() != viewport {
205 self.frame = Frame::new(viewport);
206 self.backdrop_frame = Frame::new(viewport);
207 self.backdrop_at = None;
208 }
209 let clock = elapsed;
210 let elapsed = elapsed.as_secs_f32();
211 // Exponential pointer chase, frame-rate independent (~100ms lag).
212 let delta = (elapsed - self.last_elapsed).max(0.0);
213 self.last_elapsed = elapsed;
214 let response = 1.0 - (-delta * 10.0).exp();
215 self.camera.0 += (self.camera_target.0 - self.camera.0) * response;
216 self.camera.1 += (self.camera_target.1 - self.camera.1) * response;
217 self.draw_backdrop(viewport, clock, elapsed);
218
219 let logo_interval = ambient_interval(clock, LOGO_IDLE_INTERVAL);
220 if self
221 .logo_at
222 .is_none_or(|rendered_at| clock.saturating_sub(rendered_at) >= logo_interval)
223 {
224 self.logo = logo_cells(elapsed, self.camera);
225 self.logo_at = Some(clock);
226 }
227 let cols = if viewport.width >= CARD_COLS && viewport.height >= CARD_ROWS {
228 Some(CARD_COLS)
229 } else if viewport.width >= SMOL_COLS && viewport.height >= CARD_ROWS {
230 Some(SMOL_COLS)
231 } else {
232 None
233 };
234 let Some(cols) = cols else {
235 let left = viewport.width.saturating_sub(LOGO_COLS as u16) / 2;
236 let top = viewport.height.saturating_sub(LOGO_ROWS as u16) / 2;
237 self.logo_origin = (left, top);
238 blit_logo(&mut self.frame, &self.logo, left, top, PLATE);
239 return &self.frame;
240 };
241
242 let left = (viewport.width - cols) / 2;
243 let top = (viewport.height - CARD_ROWS) / 2;
244 let hovered = self.pointer.is_some_and(|(x, y)| {
245 (left..left + cols).contains(&x) && (top..top + CARD_ROWS).contains(&y)
246 });
247 self
248 .hover
249 .retarget(clock, if hovered { 1.0 } else { 0.0 }, HOVER_EASE, Easing::EaseOut);
250 let hover = self.hover.sample(clock).clamp(0.0, 1.0);
251 self.draw_card(cols, left, top, elapsed, hover);
252 &self.frame
253 }Sourcepub fn is_settled(&self, now: Duration) -> bool
pub fn is_settled(&self, now: Duration) -> bool
Whether the tween has reached its target at time now.
Examples found in repository?
examples/chat/demo.rs (line 686)
664 fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
665 let mut left = self.left_group(pc.now);
666 let (_, left_width) = left.measure(pc.ctx);
667 let (_, right_width) = self.right.measure(pc.ctx);
668 if left_width.saturating_add(2).saturating_add(right_width) <= rect.width {
669 left.paint(pc, Rect::new(rect.x, rect.y, left_width, 1));
670 let dock = rect
671 .x
672 .saturating_add(rect.width)
673 .saturating_sub(right_width);
674 self
675 .right
676 .paint(pc, Rect::new(dock, rect.y, right_width, 1));
677 } else {
678 let mut combined = self.combined(pc.now);
679 combined.paint(pc, rect);
680 }
681 let work = self.work.borrow();
682 let fade_frame = work
683 .fade
684 .settles_at()
685 .min(pc.now.saturating_add(FADE_FRAME));
686 let deadline = match (work.working, work.fade.is_settled(pc.now)) {
687 (true, true) => Some(pc.ctx.charset.spinner().next_change(pc.now)),
688 (true, false) => Some(pc.ctx.charset.spinner().next_change(pc.now).min(fade_frame)),
689 (false, false) => Some(fade_frame),
690 (false, true) => None,
691 };
692 if let Some(at) = deadline {
693 pc.wake(self.slot, at);
694 }
695 }Sourcepub const fn settles_at(&self) -> Duration
pub const fn settles_at(&self) -> Duration
When the tween reaches its target — the deadline for the final frame.
Examples found in repository?
examples/chat/demo.rs (line 684)
664 fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
665 let mut left = self.left_group(pc.now);
666 let (_, left_width) = left.measure(pc.ctx);
667 let (_, right_width) = self.right.measure(pc.ctx);
668 if left_width.saturating_add(2).saturating_add(right_width) <= rect.width {
669 left.paint(pc, Rect::new(rect.x, rect.y, left_width, 1));
670 let dock = rect
671 .x
672 .saturating_add(rect.width)
673 .saturating_sub(right_width);
674 self
675 .right
676 .paint(pc, Rect::new(dock, rect.y, right_width, 1));
677 } else {
678 let mut combined = self.combined(pc.now);
679 combined.paint(pc, rect);
680 }
681 let work = self.work.borrow();
682 let fade_frame = work
683 .fade
684 .settles_at()
685 .min(pc.now.saturating_add(FADE_FRAME));
686 let deadline = match (work.working, work.fade.is_settled(pc.now)) {
687 (true, true) => Some(pc.ctx.charset.spinner().next_change(pc.now)),
688 (true, false) => Some(pc.ctx.charset.spinner().next_change(pc.now).min(fade_frame)),
689 (false, false) => Some(fade_frame),
690 (false, true) => None,
691 };
692 if let Some(at) = deadline {
693 pc.wake(self.slot, at);
694 }
695 }Sourcepub fn retarget(
&mut self,
now: Duration,
to: T,
duration: Duration,
easing: Easing,
)where
T: PartialEq,
pub fn retarget(
&mut self,
now: Duration,
to: T,
duration: Duration,
easing: Easing,
)where
T: PartialEq,
Redirects the tween toward to over duration, starting from the
value currently on screen. A matching target is a no-op, so callers
may retarget unconditionally on every state change.
Examples found in repository?
examples/chat/demo.rs (line 890)
879 fn set_working(&mut self, working: bool, now: Duration) {
880 {
881 let mut work = self.work.borrow_mut();
882 if work.working == working {
883 return;
884 }
885 work.working = working;
886 work.since = now;
887 let target = if working { GREEN } else { MUTED };
888 work
889 .fade
890 .retarget(now, target, BRAND_FADE, Easing::EaseInOut);
891 }
892 self.editor_ui.invalidate(STATUS_ID);
893 }More examples
examples/chat/welcome.rs (line 249)
203 pub fn render(&mut self, viewport: Size, elapsed: Duration) -> &Frame {
204 if self.frame.size() != viewport {
205 self.frame = Frame::new(viewport);
206 self.backdrop_frame = Frame::new(viewport);
207 self.backdrop_at = None;
208 }
209 let clock = elapsed;
210 let elapsed = elapsed.as_secs_f32();
211 // Exponential pointer chase, frame-rate independent (~100ms lag).
212 let delta = (elapsed - self.last_elapsed).max(0.0);
213 self.last_elapsed = elapsed;
214 let response = 1.0 - (-delta * 10.0).exp();
215 self.camera.0 += (self.camera_target.0 - self.camera.0) * response;
216 self.camera.1 += (self.camera_target.1 - self.camera.1) * response;
217 self.draw_backdrop(viewport, clock, elapsed);
218
219 let logo_interval = ambient_interval(clock, LOGO_IDLE_INTERVAL);
220 if self
221 .logo_at
222 .is_none_or(|rendered_at| clock.saturating_sub(rendered_at) >= logo_interval)
223 {
224 self.logo = logo_cells(elapsed, self.camera);
225 self.logo_at = Some(clock);
226 }
227 let cols = if viewport.width >= CARD_COLS && viewport.height >= CARD_ROWS {
228 Some(CARD_COLS)
229 } else if viewport.width >= SMOL_COLS && viewport.height >= CARD_ROWS {
230 Some(SMOL_COLS)
231 } else {
232 None
233 };
234 let Some(cols) = cols else {
235 let left = viewport.width.saturating_sub(LOGO_COLS as u16) / 2;
236 let top = viewport.height.saturating_sub(LOGO_ROWS as u16) / 2;
237 self.logo_origin = (left, top);
238 blit_logo(&mut self.frame, &self.logo, left, top, PLATE);
239 return &self.frame;
240 };
241
242 let left = (viewport.width - cols) / 2;
243 let top = (viewport.height - CARD_ROWS) / 2;
244 let hovered = self.pointer.is_some_and(|(x, y)| {
245 (left..left + cols).contains(&x) && (top..top + CARD_ROWS).contains(&y)
246 });
247 self
248 .hover
249 .retarget(clock, if hovered { 1.0 } else { 0.0 }, HOVER_EASE, Easing::EaseOut);
250 let hover = self.hover.sample(clock).clamp(0.0, 1.0);
251 self.draw_card(cols, left, top, elapsed, hover);
252 &self.frame
253 }Trait Implementations§
Auto Trait Implementations§
impl<T> Freeze for Tween<T>where
T: Freeze,
impl<T> RefUnwindSafe for Tween<T>where
T: RefUnwindSafe,
impl<T> Send for Tween<T>where
T: Send,
impl<T> Sync for Tween<T>where
T: Sync,
impl<T> Unpin for Tween<T>where
T: Unpin,
impl<T> UnsafeUnpin for Tween<T>where
T: UnsafeUnpin,
impl<T> UnwindSafe for Tween<T>where
T: UnwindSafe,
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Convert
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Convert
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
Convert
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
Convert
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
Converts
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
Converts
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more