Skip to main content

Tween

Struct Tween 

Source
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>

Source

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
Hide additional 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	}
Source

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
Hide additional 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	}
Source

pub const fn target(&self) -> T

The value the tween is heading toward.

Source

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	}
Source

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	}
Source

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
Hide additional 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§

Source§

impl<T: Clone + Lerp> Clone for Tween<T>

Source§

fn clone(&self) -> Tween<T>

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl<T: Copy + Lerp> Copy for Tween<T>

Source§

impl<T: Debug + Lerp> Debug for Tween<T>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

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> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

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>

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)

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)

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
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

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
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.