Files
Ely-GPUI-Components/src/interaction/sync.rs
T
ZacharyZhang-NYandClaude Opus 5.5 9e3e6c109d Interaction · Keys, scroll and focus: roving focus, scroll sync, type-ahead
RovingFocus keeps a row or column of items behind one Tab stop: the
arrows walk focus, Home and End reach the ends, Enter or Space presses,
and a press moves the stop without taking focus. ScrollSync keeps boxes
at one place along an axis: the pane a wheel turned leads, and a canvas
ahead of the panes copies its place before they draw. SelectableList
moves its cursor to the row that starts with the letters typed, the
letters joining within a pause and a Command letter left to the app.
forms::History becomes public as the undo manager. The gallery shows
gpui's own key bindings, a two-stroke chord waiting on its first
stroke, uniform_list, autofocus and the focus ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:10:32 -04:00

134 lines
4.3 KiB
Rust

use gpui::{
AnyElement, App, Axis, Div, ElementId, InteractiveElement, IntoElement, ParentElement, Pixels,
Point, RenderOnce, ScrollHandle, StatefulInteractiveElement, StyleRefinement, Styled, Window,
canvas, div, point, prelude::*,
};
use crate::layout::on_axis;
/// `to` with its place along `axis` taken from `from`.
fn along(to: Point<Pixels>, from: Point<Pixels>, axis: Axis) -> Point<Pixels> {
match axis {
Axis::Vertical => point(to.x, from.y),
Axis::Horizontal => point(from.x, to.y),
}
}
/// Boxes that scroll together along an axis: a wheel on one takes the others to its place before they draw. Side by side for a vertical axis, stacked for a horizontal one; the owner sizes them through the frame's style, and each fills its share.
#[derive(IntoElement)]
pub struct ScrollSync {
id: ElementId,
axis: Axis,
base: Div,
panes: Vec<AnyElement>,
}
impl ScrollSync {
pub fn new(id: impl Into<ElementId>, axis: Axis) -> Self {
Self {
id: id.into(),
axis,
base: div(),
panes: Vec::new(),
}
}
pub fn pane(mut self, content: impl IntoElement) -> Self {
self.panes.push(content.into_any_element());
self
}
}
impl Styled for ScrollSync {
fn style(&mut self) -> &mut StyleRefinement {
self.base.style()
}
}
impl RenderOnce for ScrollSync {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let (id, axis) = (self.id, self.axis);
assert!(self.panes.len() >= 2, "scroll sync {id:?} needs two panes");
let handles: Vec<ScrollHandle> = (0..self.panes.len())
.map(|ix| {
window
.use_keyed_state((id.clone(), format!("pane-{ix}")), cx, |_, _| {
ScrollHandle::new()
})
.read(cx)
.clone()
})
.collect();
let leader = window.use_keyed_state((id.clone(), "leader"), cx, |_, _| None::<usize>);
let (lead, synced) = (*leader.read(cx), handles.clone());
let sync = canvas(
move |_, _, _| {
let Some(lead) = lead else { return };
let place = synced[lead].offset();
for (ix, handle) in synced.iter().enumerate() {
let now = handle.offset();
let next = along(now, place, axis);
if ix != lead && next != now {
handle.set_offset(next);
}
}
},
|_, _, _, _| {},
)
.absolute()
.top_0()
.left_0()
.size_0();
let panes =
self.panes
.into_iter()
.zip(handles)
.enumerate()
.map(|(ix, (content, handle))| {
let wheel = leader.clone();
let pane = div()
.id((id.clone(), format!("pane-{ix}")))
.flex_1()
.track_scroll(&handle)
.on_scroll_wheel(move |_, _, cx| {
if *wheel.read(cx) != Some(ix) {
log::info!("scroll sync: pane {ix} leads");
wheel.update(cx, |lead, _| *lead = Some(ix));
}
})
.child(content);
on_axis(match axis {
Axis::Vertical => pane.min_w_0().overflow_y_scroll(),
Axis::Horizontal => pane.min_h_0().overflow_x_scroll(),
})
});
self.base
.id(id.clone())
.relative()
.flex()
.when(axis == Axis::Horizontal, |frame| frame.flex_col())
.child(sync)
.children(panes)
}
}
#[cfg(test)]
mod tests {
use gpui::{Axis, point, px};
use super::along;
#[test]
fn a_place_follows_only_along_the_axis() {
let (mine, theirs) = (point(px(5.0), px(-20.0)), point(px(-40.0), px(-90.0)));
assert_eq!(
along(mine, theirs, Axis::Vertical),
point(px(5.0), px(-90.0))
);
assert_eq!(
along(mine, theirs, Axis::Horizontal),
point(px(-40.0), px(-20.0))
);
}
}