Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
327 changes: 318 additions & 9 deletions crates/base/src/plot/appear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@
//! This is behavior only. A plot decides what appearing looks like — a line
//! revealed from the left, bars growing from zero — and a styled layer
//! projects the timing through [`PlotMotion`](crate::PlotMotion).
use gpui::{App, ElementId, Window};
//!
//! The appear lives in the plot's element state, so a plot that stops being
//! painted — a row a virtual list scrolled away — forgets it and draws in again
//! when it comes back. A [`PlotAppearScope`] around the list remembers which
//! plots have finished, for as long as the scope itself is painted.
use std::{cell::RefCell, collections::HashMap, panic::Location, rc::Rc};

use gpui::{
AnyElement, App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
LayoutId, Pixels, Window,
};

use crate::{
Theme,
Expand Down Expand Up @@ -69,12 +79,160 @@ impl PlotAppear {
/// The element-state key of a plot's appear, within the plot's scope.
const APPEAR: &str = "__plot-appear";

/// The plots a [`PlotAppearScope`] has seen finish appearing, by global id,
/// with the generation that finished.
type Appeared = Rc<RefCell<HashMap<GlobalElementId, u64>>>;

thread_local! {
/// The scopes being laid out or painted, innermost last. Only non-empty
/// while a [`PlotAppearScope`] is drawing its child.
static SCOPES: RefCell<Vec<Appeared>> = const { RefCell::new(Vec::new()) };
}

/// Remembers which plots inside it have finished appearing, so a plot that is
/// painted again after a gap — a row a virtual list scrolled out of view and
/// back — shows its data whole instead of drawing in again.
///
/// Wrap the list, or whatever region repaints its plots on and off, in one:
///
/// ```ignore
/// PlotAppearScope::new(("transcript", conversation_id), list(state, render_row).flex_1())
/// ```
///
/// A plot is remembered by its global element id and its
/// [`Plot::appear_generation`](super::Plot::appear_generation), once its appear
/// has finished; a new generation still replays it, and one taken away
/// mid-appear draws in again from the start. The memory is the scope's own
/// element state: it lasts while the scope is painted every frame and goes with
/// it, so a view that is closed, or a scope whose id changes — name it after the
/// content, such as a conversation — draws its plots in afresh. The innermost
/// scope wins. Without one, every plot draws in each time it is painted anew.
///
/// The scope takes no part in layout: it hands on its child's layout, so a
/// child that sizes itself, such as a `list`, keeps doing so.
pub struct PlotAppearScope {
id: ElementId,
child: Option<AnyElement>,
}

impl PlotAppearScope {
/// Remember the appears of the plots in `child` under `id`, unique among
/// its siblings.
pub fn new(id: impl Into<ElementId>, child: impl IntoElement) -> Self {
Self {
id: id.into(),
child: Some(child.into_any_element()),
}
}

/// Run `f` with `appeared` as the innermost scope.
fn within<R>(appeared: &Appeared, f: impl FnOnce() -> R) -> R {
SCOPES.with_borrow_mut(|scopes| scopes.push(appeared.clone()));
let result = f();
SCOPES.with_borrow_mut(|scopes| scopes.pop());
result
}
}

impl IntoElement for PlotAppearScope {
type Element = Self;

fn into_element(self) -> Self::Element {
self
}
}

impl Element for PlotAppearScope {
type RequestLayoutState = (Option<AnyElement>, Appeared);
type PrepaintState = ();

fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}

fn source_location(&self) -> Option<&'static Location<'static>> {
None
}

fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let appeared: Appeared = match global_id {
Some(global_id) => window.with_element_state(global_id, |appeared, _| {
let appeared: Appeared = appeared.unwrap_or_default();
(appeared.clone(), appeared)
}),
None => Appeared::default(),
};
let mut child = self.child.take();
let layout_id = Self::within(&appeared, || match child.as_mut() {
Some(child) => child.request_layout(window, cx),
None => window.request_layout(Default::default(), None, cx),
});
(layout_id, (child, appeared))
}

fn prepaint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
(child, appeared): &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
// A virtual list lays its rows out here, so their plots appear here.
if let Some(child) = child {
Self::within(appeared, || {
child.prepaint(window, cx);
});
}
}

fn paint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
(child, appeared): &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
if let Some(child) = child {
Self::within(appeared, || child.paint(window, cx));
}
}
}

/// The innermost scope, if any, cloned out so no borrow outlives the lookup.
fn current_scope() -> Option<Appeared> {
SCOPES.with_borrow(|scopes| scopes.last().cloned())
}

/// Sample the appear of the plot painting under the window's current element
/// id. A new `generation` starts it over. Called by
/// id, `global_id`. A new `generation` starts it over, and one the innermost
/// [`PlotAppearScope`] saw finish is complete at once. Called by
/// [`PlotElement`](super::PlotElement) within the plot's element scope on every
/// frame, so it borrows the theme rather than cloning it and builds its key
/// without allocating.
pub(super) fn track_appear(generation: u64, window: &mut Window, cx: &mut App) -> PlotAppear {
pub(super) fn track_appear(
global_id: &GlobalElementId,
generation: u64,
window: &mut Window,
cx: &mut App,
) -> PlotAppear {
let scope = current_scope();
if scope
.as_ref()
.is_some_and(|scope| scope.borrow().get(global_id) == Some(&generation))
{
return PlotAppear::complete();
}
let Some(policy) = cx
.try_global::<Theme>()
.map(|theme| theme.plot.motion().appear().clone())
Expand All @@ -87,6 +245,11 @@ pub(super) fn track_appear(generation: u64, window: &mut Window, cx: &mut App) -
let sample = Presence::new((ElementId::Integer(generation), APPEAR), true)
.transition(policy.easing(Easing::Linear))
.sample(window, cx);
if sample.progress >= 1.
&& let Some(scope) = scope
{
scope.borrow_mut().insert(global_id.clone(), generation);
}
PlotAppear {
time: sample.progress,
easing,
Expand All @@ -98,8 +261,8 @@ mod tests {
use std::{cell::RefCell, rc::Rc, time::Duration};

use gpui::{
Bounds, Context, ElementId, IntoElement, Pixels, Render, TestAppContext, WindowHandle, px,
size,
Bounds, Context, ElementId, IntoElement, ParentElement as _, Pixels, Render, Styled as _,
TestAppContext, WindowHandle, px, size,
};

use super::*;
Expand Down Expand Up @@ -158,17 +321,91 @@ mod tests {
}
}

fn open(
cx: &mut TestAppContext,
generation: Option<u64>,
) -> (WindowHandle<RecorderView>, Rc<RefCell<Vec<f32>>>) {
/// A recorder that can be taken out of the tree and put back, inside a
/// [`PlotAppearScope`] named `scope` or none.
struct ScopedView {
samples: Rc<RefCell<Vec<f32>>>,
scope: Option<usize>,
mounted: bool,
generation: u64,
}

impl Render for ScopedView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let plot = self.mounted.then(|| Recorder {
samples: self.samples.clone(),
generation: Some(self.generation),
});
let body = gpui::div().size_full().children(plot);
match self.scope {
Some(scope) => PlotAppearScope::new(("scope", scope), body).into_any_element(),
None => body.into_any_element(),
}
}
}

fn set_theme(cx: &mut TestAppContext) {
cx.update(|cx| {
cx.set_global(Theme::default());
Theme::global_mut(cx).plot = PlotTheme::new().with_motion(
PlotMotion::default()
.with_appear(Transition::new(Duration::from_millis(100)).ease(|t| t)),
);
});
}

fn open_scoped(
cx: &mut TestAppContext,
scope: Option<usize>,
) -> (WindowHandle<ScopedView>, Rc<RefCell<Vec<f32>>>) {
set_theme(cx);
let samples = Rc::new(RefCell::new(Vec::new()));
let window = cx.open_window(size(px(100.), px(100.)), {
let samples = samples.clone();
move |_, _| ScopedView {
samples,
scope,
mounted: true,
generation: 0,
}
});
cx.run_until_parked();
(window, samples)
}

/// Change the view, then return the progress the plot was handed on the
/// frame that follows, if it was painted.
fn update_scoped(
window: WindowHandle<ScopedView>,
cx: &mut TestAppContext,
f: impl FnOnce(&mut ScopedView),
) -> Option<f32> {
window
.update(cx, |view, _, cx| {
f(view);
view.samples.borrow_mut().clear();
cx.notify();
})
.unwrap();
cx.run_until_parked();
window
.update(cx, |view, _, _| view.samples.borrow().last().copied())
.unwrap()
}

fn finish_appear(window: WindowHandle<ScopedView>, cx: &mut TestAppContext) {
cx.executor().advance_clock(Duration::from_millis(100));
window
.update(cx, |_, window, cx| window.simulate_next_frame(cx))
.unwrap();
cx.run_until_parked();
}

fn open(
cx: &mut TestAppContext,
generation: Option<u64>,
) -> (WindowHandle<RecorderView>, Rc<RefCell<Vec<f32>>>) {
set_theme(cx);
let samples = Rc::new(RefCell::new(Vec::new()));
let window = cx.open_window(size(px(100.), px(100.)), {
let samples = samples.clone();
Expand Down Expand Up @@ -244,6 +481,78 @@ mod tests {
assert_eq!(samples.borrow().last(), Some(&0.));
}

/// Inside a scope, a plot painted again after a gap is whole at once and
/// asks for no frames.
#[gpui::test]
fn test_scope_keeps_a_finished_appear_across_a_remount(cx: &mut TestAppContext) {
let (window, samples) = open_scoped(cx, Some(0));
assert_eq!(samples.borrow().last(), Some(&0.));
finish_appear(window, cx);
assert_eq!(samples.borrow().last(), Some(&1.));

assert_eq!(update_scoped(window, cx, |view| view.mounted = false), None);
assert_eq!(
update_scoped(window, cx, |view| view.mounted = true),
Some(1.)
);
let frames = window
.update(cx, |_, window, cx| window.simulate_next_frame(cx))
.unwrap();
assert_eq!(frames, 0);

// A new generation still replays.
assert_eq!(
update_scoped(window, cx, |view| view.generation = 1),
Some(0.)
);
}

/// A plot taken away before its appear finished draws in again.
#[gpui::test]
fn test_scope_replays_an_unfinished_appear(cx: &mut TestAppContext) {
let (window, _) = open_scoped(cx, Some(0));
assert_eq!(update_scoped(window, cx, |view| view.mounted = false), None);
assert_eq!(
update_scoped(window, cx, |view| view.mounted = true),
Some(0.)
);
}

/// The memory goes with the scope: a scope under a new id, or one that
/// stops being painted, draws its plots in afresh.
#[gpui::test]
fn test_scope_forgets_when_it_goes(cx: &mut TestAppContext) {
let (window, _) = open_scoped(cx, Some(0));
finish_appear(window, cx);
assert_eq!(
update_scoped(window, cx, |view| view.scope = Some(1)),
Some(0.)
);

finish_appear(window, cx);
assert_eq!(
update_scoped(window, cx, |view| view.scope = None),
Some(0.)
);
finish_appear(window, cx);
assert_eq!(
update_scoped(window, cx, |view| view.scope = Some(1)),
Some(0.)
);
}

/// Without a scope, a plot painted again after a gap draws in again.
#[gpui::test]
fn test_without_a_scope_a_remount_replays(cx: &mut TestAppContext) {
let (window, _) = open_scoped(cx, None);
finish_appear(window, cx);
assert_eq!(update_scoped(window, cx, |view| view.mounted = false), None);
assert_eq!(
update_scoped(window, cx, |view| view.mounted = true),
Some(0.)
);
}

fn at(time: f32) -> PlotAppear {
PlotAppear {
time,
Expand Down
2 changes: 1 addition & 1 deletion crates/base/src/plot/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ impl<P: Plot + 'static> Element for PlotElement<P> {
};

let appear = match self.0.appear_generation() {
Some(generation) => track_appear(generation, window, cx),
Some(generation) => track_appear(global_id, generation, window, cx),
None => PlotAppear::complete(),
};
let appearing = appear.is_appearing();
Expand Down
2 changes: 1 addition & 1 deletion crates/base/src/plot/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use gpui::{

use crate::{Spring, motion::Transition};

pub use appear::PlotAppear;
pub use appear::{PlotAppear, PlotAppearScope};
#[allow(deprecated)]
pub use axis::AXIS_GAP;
pub use axis::{AxisLabelPlacement, AxisLabelSide, AxisText, PlotAxis, axis_gutter};
Expand Down
Loading
Loading