Skip to main content

gpui_ce_motion/
shared_layout.rs

1//! Explicit shared identity in a common coordinate space, without size projection.
2use crate::{LayoutPositionState, MotionValue, Transition};
3
4use gpui::{
5    AnyElement, App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
6    LayoutId, Pixels, Styled, Window, point, px,
7};
8
9use std::{cell::RefCell, collections::HashMap, hash::Hash, rc::Rc, time::Instant};
10
11struct Entry {
12    position: LayoutPositionState,
13    destination: [f64; 2],
14    member: Option<GlobalElementId>,
15    epoch: Instant,
16    measured: bool,
17}
18
19/// View-owned shared identity registry. Exactly one active destination per key
20/// is required. Render incoming/outgoing appearances in a common overlay; retain
21/// outgoing content until its normal presence exit completes. Native clipping
22/// still applies, sizes change immediately, and independent scrolling coordinate
23/// spaces are not reconciled. This is position/crossfade, not full `layoutId`.
24/// Clones share storage. Call `remove` when a logical shared identity is finished.
25#[derive(Clone)]
26pub struct SharedLayout<K>(Rc<RefCell<HashMap<K, Entry>>>);
27
28impl<K> Default for SharedLayout<K> {
29    fn default() -> Self {
30        Self(Rc::new(RefCell::new(HashMap::new())))
31    }
32}
33
34impl<K: Eq + Hash + Clone + 'static> SharedLayout<K> {
35    /// Forgets a shared identity. Existing native member history ends on unmount.
36    pub fn remove(&self, key: &K) {
37        self.0.borrow_mut().remove(key);
38    }
39
40    /// Number of explicitly retained shared identities.
41    pub fn len(&self) -> usize {
42        self.0.borrow().len()
43    }
44
45    /// Whether no identities are retained.
46    pub fn is_empty(&self) -> bool {
47        self.0.borrow().is_empty()
48    }
49
50    /// Creates a crossfading appearance. Styles and absolute positioning belong
51    /// on the child. Inactive retained appearances follow the active position.
52    /// The owner must suppress input and restore focus for outgoing appearances,
53    /// just as with normal presence exits; opacity does not disable handlers.
54    ///
55    /// ```
56    /// use gpui::{div, prelude::*, px};
57    /// use gpui_ce_motion::SharedLayout;
58    /// let shared = SharedLayout::<u32>::default(); // persistent view field
59    /// let first = shared.member(1, "old", false, div().absolute().left(px(0.)).child("Old"));
60    /// let second = shared.member(1, "new", true, div().absolute().left(px(200.)).child("New"));
61    /// let overlay = div().relative().child(first).child(second);
62    /// ```
63    pub fn member<E: IntoElement + Styled + 'static>(
64        &self,
65        key: K,
66        id: impl Into<ElementId>,
67        active: bool,
68        child: E,
69    ) -> SharedElement<K, E> {
70        SharedElement {
71            group: self.clone(),
72            key,
73            id: id.into(),
74            active,
75            child: Some(child),
76            transition: Transition::default(),
77        }
78    }
79}
80
81/// Native position/crossfade appearance constructed by `SharedLayout::member`.
82pub struct SharedElement<K, E> {
83    group: SharedLayout<K>,
84    key: K,
85    id: ElementId,
86    active: bool,
87    child: Option<E>,
88    transition: Transition,
89}
90
91impl<K, E> SharedElement<K, E> {
92    /// Chooses interruption-aware positional and crossfade timing.
93    pub fn transition(mut self, transition: Transition) -> Self {
94        self.transition = transition;
95
96        self
97    }
98}
99
100struct Fade {
101    value: MotionValue,
102    epoch: Instant,
103}
104
105impl<K: Eq + Hash + Clone + 'static, E: IntoElement + Styled + 'static> IntoElement
106    for SharedElement<K, E>
107{
108    type Element = Self;
109
110    fn into_element(self) -> Self {
111        self
112    }
113}
114
115impl<K: Eq + Hash + Clone + 'static, E: IntoElement + Styled + 'static> Element
116    for SharedElement<K, E>
117{
118    type RequestLayoutState = AnyElement;
119    type PrepaintState = ();
120
121    fn id(&self) -> Option<ElementId> {
122        Some(self.id.clone())
123    }
124
125    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
126        None
127    }
128
129    fn request_layout(
130        &mut self,
131        id: Option<&GlobalElementId>,
132        _: Option<&InspectorElementId>,
133        w: &mut Window,
134        cx: &mut App,
135    ) -> (LayoutId, AnyElement) {
136        let now = cx.background_executor().now();
137        let id = id.expect("shared ID");
138        let mut entries = self.group.0.borrow_mut();
139
140        let entry = entries.entry(self.key.clone()).or_insert_with(|| Entry {
141            position: LayoutPositionState::default(),
142            destination: [0.; 2],
143            member: None,
144            epoch: now,
145            measured: false,
146        });
147
148        let initial = if self.active && entry.member.is_none() {
149            1.
150        } else {
151            0.
152        };
153
154        if self.active {
155            entry.member = Some(id.clone());
156        }
157
158        drop(entries);
159        let opacity = w.with_element_state(id, |state: Option<Fade>, w| {
160            let mut state = state.unwrap_or_else(|| Fade {
161                value: MotionValue::new(initial),
162                epoch: now,
163            });
164
165            let time = now.saturating_duration_since(state.epoch);
166
167            state
168                .value
169                .animate(if self.active { 1. } else { 0. }, self.transition, time);
170            let frame = state.value.sample(time, cx.reduce_motion());
171
172            if frame.is_animating() {
173                w.request_animation_frame();
174            }
175
176            (frame.value().clamp(0., 1.) as f32, state)
177        });
178
179        let mut child = self
180            .child
181            .take()
182            .expect("one layout")
183            .opacity(opacity)
184            .into_any_element();
185
186        let layout = child.request_layout(w, cx);
187
188        (layout, child)
189    }
190
191    fn prepaint(
192        &mut self,
193        _: Option<&GlobalElementId>,
194        _: Option<&InspectorElementId>,
195        bounds: Bounds<Pixels>,
196        child: &mut AnyElement,
197        w: &mut Window,
198        cx: &mut App,
199    ) {
200        let now = cx.background_executor().now();
201        let mut entries = self.group.0.borrow_mut();
202        let entry = entries.get_mut(&self.key).expect("layout first");
203
204        if !entry.measured && !self.active {
205            drop(entries);
206            child.prepaint(w, cx);
207
208            return;
209        }
210
211        if self.active {
212            entry.measured = true;
213            entry.destination = [f64::from(bounds.origin.x), f64::from(bounds.origin.y)];
214        }
215
216        let frame = entry.position.sample(
217            entry.destination,
218            self.transition,
219            now.saturating_duration_since(entry.epoch),
220            cx.reduce_motion(),
221        );
222
223        if frame.is_animating() {
224            w.request_animation_frame();
225        }
226
227        let presented = frame.position();
228
229        let offset = point(
230            px((presented[0] - f64::from(bounds.origin.x))
231                .clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32),
232            px((presented[1] - f64::from(bounds.origin.y))
233                .clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32),
234        );
235
236        drop(entries);
237        w.with_element_offset(offset, |w| {
238            child.prepaint(w, cx);
239        });
240    }
241
242    fn paint(
243        &mut self,
244        _: Option<&GlobalElementId>,
245        _: Option<&InspectorElementId>,
246        _: Bounds<Pixels>,
247        child: &mut AnyElement,
248        _: &mut (),
249        w: &mut Window,
250        cx: &mut App,
251    ) {
252        child.paint(w, cx);
253    }
254}