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
76 changes: 68 additions & 8 deletions crates/base/src/dock/dock_area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use gpui::{
AnyElement, AnyView, App, AppContext as _, Axis, Bounds, Context, Div, Empty, Entity,
EventEmitter, FocusHandle, Focusable, Hsla, InteractiveElement as _, IntoElement,
ParentElement, Pixels, Point, Render, SharedString, Stateful, Styled as _, Subscription,
WeakEntity, Window, WindowHandle, WindowOptions, div, prelude::FluentBuilder as _, px,
WeakEntity, Window, WindowHandle, WindowId, WindowOptions, div, prelude::FluentBuilder as _,
px,
};

use crate::{
Expand Down Expand Up @@ -101,7 +102,15 @@ struct CachedSplit {
pub struct DockArea {
id: SharedString,
version: Option<usize>,
/// Bounds in the measurement window, or in the last window painted when
/// none is named.
bounds: Bounds<Pixels>,
/// Bounds in each window the area is drawn in. A dock resize measures
/// against the window the pointer is in.
window_bounds: HashMap<WindowId, Bounds<Pixels>>,
/// The one window whose layout writes measured split geometry. See
/// [`Self::set_measurement_window`].
measurement_window: Option<WindowId>,
this: WeakEntity<Self>,

center: PaneTree,
Expand Down Expand Up @@ -140,6 +149,8 @@ impl DockArea {
id: id.into(),
version,
bounds: Bounds::default(),
window_bounds: HashMap::new(),
measurement_window: None,
this: cx.weak_entity(),
center: PaneTree::new(RootKind::Split),
docks: HashMap::new(),
Expand Down Expand Up @@ -186,12 +197,48 @@ impl DockArea {
cx.notify();
}

/// The area's own bounds, recorded each frame. Dock resizing measures
/// against it.
/// The area's own bounds, recorded each frame in the measurement window
/// (in whichever window painted last when none is named).
pub fn bounds(&self) -> Bounds<Pixels> {
self.bounds
}

/// Name the window whose layout the area's shared split sizes follow.
///
/// The splits' measured sizes live in one state per split, shared by every
/// window the area is drawn in. When two differently sized windows both
/// write their measurements into it, each makes the other's layout stale
/// and they redraw each other forever. A host that draws one area in
/// several windows names the one that measures; the others draw the
/// splits at the sizes it measured. `None`, the default, measures in
/// every window, which is right for an area drawn in only one.
///
/// Takes effect at the next draw of each window and schedules nothing,
/// so a host may call it while rendering.
pub fn set_measurement_window(&mut self, window: Option<WindowId>) {
self.measurement_window = window;
}

/// The window named by [`Self::set_measurement_window`].
pub fn measurement_window(&self) -> Option<WindowId> {
self.measurement_window
}

fn measures_in(&self, window: WindowId) -> bool {
self.measurement_window.is_none_or(|owner| owner == window)
}

fn record_bounds(&mut self, window: WindowId, bounds: Bounds<Pixels>, cx: &App) {
if self.measures_in(window) {
self.bounds = bounds;
}
if self.window_bounds.insert(window, bounds).is_none() {
// A window is new here: forget any that have since closed.
let live: HashSet<WindowId> = cx.windows().iter().map(|w| w.window_id()).collect();
self.window_bounds.retain(|id, _| live.contains(id));
}
}

/// The tree for one region, or `None` for a dock that does not exist.
///
/// The `Option` is in the signature rather than hidden behind a panic
Expand Down Expand Up @@ -1484,15 +1531,23 @@ impl DockArea {
&mut self,
placement: DockPlacement,
pointer: Point<Pixels>,
window: WindowId,
cx: &mut Context<Self>,
) {
let opposite = match placement {
DockPlacement::Left => self.dock_size(DockPlacement::Right),
DockPlacement::Right => self.dock_size(DockPlacement::Left),
_ => None,
};
// The pointer is in `window`'s coordinates, so it is measured against
// the area as that window laid it out.
let area_bounds = self
.window_bounds
.get(&window)
.copied()
.unwrap_or(self.bounds);
let sizing = DockSizing::new(placement)
.with_area_bounds(self.bounds)
.with_area_bounds(area_bounds)
.with_opposite_dock_size(opposite.unwrap_or(px(0.)));
let size = sizing.clamp(sizing.size_from_pointer(pointer));

Expand Down Expand Up @@ -1561,6 +1616,7 @@ impl DockArea {
.when_some(self.splits.get(&node.id()), |group, cached| {
group.with_state(&cached.entity)
})
.measure(self.measures_in(window.window_handle().window_id()))
.with_handle_appearance({
let renderer = self.renderer.clone();
Rc::new(move |handle, window, cx| {
Expand Down Expand Up @@ -1652,8 +1708,11 @@ impl DockArea {
_ = area.update(cx, |area, cx| area.toggle_dock(placement, window, cx));
})
},
on_resize: Rc::new(move |pointer, _, cx| {
_ = area.update(cx, |area, cx| area.resize_dock(placement, pointer, cx));
on_resize: Rc::new(move |pointer, window, cx| {
let window = window.window_handle().window_id();
_ = area.update(cx, |area, cx| {
area.resize_dock(placement, pointer, window, cx)
});
}),
}
}
Expand Down Expand Up @@ -1686,8 +1745,9 @@ impl Render for DockArea {
.overflow_hidden()
.flex()
.flex_row()
.on_prepaint(move |bounds, _, cx| {
area.update(cx, |area, _| area.bounds = bounds);
.on_prepaint(move |bounds, window, cx| {
let window = window.window_handle().window_id();
area.update(cx, |area, cx| area.record_bounds(window, bounds, cx));
})
.track_focus(&self.focus_handle)
.map(|frame| match self.zoomed_view() {
Expand Down
14 changes: 13 additions & 1 deletion crates/base/src/input/base/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1685,6 +1685,9 @@ pub(super) struct PrepaintState {
/// First line of inline completion (painted after cursor on same line)
ghost_first_line: Option<ShapedLine>,
ghost_lines_height: Pixels,
/// Whether this window writes the state's geometry back at paint. See
/// `InputBaseState::measures_in`.
measures: bool,
}

impl PrepaintState {
Expand Down Expand Up @@ -1810,6 +1813,7 @@ impl<M: InputModeKind> Element for TextElement<M> {
});

let state = self.state.read(cx);
let measures = state.measures_in(window, cx);
let multi_line = state.is_multi_line();
let text = state.text.clone();
let is_empty = text.len() == 0;
Expand Down Expand Up @@ -1857,7 +1861,9 @@ impl<M: InputModeKind> Element for TextElement<M> {
.map(|l| l.wrapping_indent != wrapping_indent)
.unwrap_or(true);

if wrap_width_changed || wrapping_indent_changed {
// Another window's wrap stays: rewrapping here would change the lines
// that window laid out and draws from.
if measures && (wrap_width_changed || wrapping_indent_changed) {
self.state.update(cx, |state, cx| {
state.display_map.on_layout_changed(wrap_width, cx);
state.display_map.set_wrapping_indent(wrapping_indent, cx);
Expand Down Expand Up @@ -2195,6 +2201,7 @@ impl<M: InputModeKind> Element for TextElement<M> {
ghost_first_line,
ghost_lines,
ghost_lines_height,
measures,
}
}

Expand Down Expand Up @@ -2485,7 +2492,12 @@ impl<M: InputModeKind> Element for TextElement<M> {
cx,
);

let window_id = window.window_handle().window_id();
self.state.update(cx, |state, cx| {
if !prepaint.measures {
return;
}
state.geometry_window = Some(window_id);
let geometry_changed = state.last_bounds != Some(bounds)
|| state.input_bounds != input_bounds
|| state.scroll_size != prepaint.scroll_size
Expand Down
120 changes: 119 additions & 1 deletion crates/base/src/input/base/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use gpui::{
EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point,
Render, ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription,
UTF16Selection, Window, actions, div, point, prelude::FluentBuilder as _, px,
UTF16Selection, Window, WindowId, actions, div, point, prelude::FluentBuilder as _, px,
};
use ropey::{Rope, RopeSlice};
use serde::Deserialize;
Expand Down Expand Up @@ -368,6 +368,9 @@ pub struct InputBaseState<M: InputModeKind> {
pub(super) input_bounds: Bounds<Pixels>,
/// The text bounds
pub(super) last_bounds: Option<Bounds<Pixels>>,
/// The window whose layout last wrote the geometry above. See
/// [`Self::measures_in`].
pub(super) geometry_window: Option<WindowId>,
pub(super) last_selected_range: Option<CursorSelection>,
pub(super) selecting: bool,
/// Anchor point of an in-progress columnar (block) selection.
Expand Down Expand Up @@ -714,6 +717,7 @@ impl<M: InputModeKind> InputBaseState<M> {
mode: LayoutMode::default(),
last_layout: None,
last_bounds: None,
geometry_window: None,
last_selected_range: None,
column_select_start: None,
last_cursor: None,
Expand Down Expand Up @@ -3382,6 +3386,26 @@ impl<M: InputModeKind> InputBaseState<M> {
};
}

/// Whether drawing in `window` may write this input's geometry: its
/// soft-wrap width, laid-out lines, bounds and scroll.
///
/// A host can draw one state in several windows at different sizes. It
/// holds one geometry, and each window writing its own would leave the
/// other's stale, so the windows would redraw each other forever. The
/// window that last measured keeps measuring until another window is
/// active or it closes; the rest draw with the geometry it measured.
pub(super) fn measures_in(&self, window: &Window, cx: &App) -> bool {
let Some(owner) = self.geometry_window else {
return true;
};
owner == window.window_handle().window_id()
|| window.is_window_active()
|| !cx
.windows()
.iter()
.any(|handle| handle.window_id() == owner)
}

pub(super) fn set_input_bounds(&mut self, new_bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
let wrap_width_changed = self.input_bounds.size.width != new_bounds.size.width;
self.input_bounds = new_bounds;
Expand Down Expand Up @@ -4392,6 +4416,100 @@ mod tests {
}
}

/// One textarea drawn in two differently sized windows holds one geometry.
/// When both windows wrote theirs at paint, each paint changed the
/// geometry the other had written, notified, and dirtied the other
/// window, so the pair redrew each other forever. The active window
/// measures; the other draws with its geometry, and both settle.
#[gpui::test]
fn test_textarea_shared_by_two_windows_settles(cx: &mut TestAppContext) {
use std::{cell::Cell, rc::Rc};

cx.update(|cx| {
cx.set_global(Theme::default());
super::super::init(cx);
});
let mut input = None;
let wide = cx.open_window(size(px(900.), px(200.)), |window, cx| {
let state = cx.new(|cx| crate::input::TextareaState::new(window, cx));
input = Some(state.clone());
TestRoot(state)
});
let input = input.unwrap();
let narrow = cx.open_window(size(px(500.), px(200.)), {
let input = input.clone();
move |_, _| TestRoot(input)
});
let notifications = Rc::new(Cell::new(0usize));
let _subscription = cx.update(|cx| {
let notifications = notifications.clone();
cx.observe(&input, move |_, _| {
let n = notifications.get() + 1;
notifications.set(n);
assert!(
n < 64,
"a shared textarea keeps notifying: its windows never settle"
);
})
});
wide.update(cx, |_, window, cx| {
window.activate_window();
input.update(cx, |state, cx| {
state.set_value(
"one textarea shown in two windows at two widths ".repeat(8),
window,
cx,
)
});
})
.unwrap();
let handles = [*wide, *narrow];
let draw_both = |cx: &mut TestAppContext| {
for handle in handles {
cx.update_window(handle, |_, window, cx| window.draw(cx).clear(cx))
.unwrap();
}
cx.run_until_parked();
};
let measured = |cx: &mut TestAppContext| {
input.read_with(cx, |state, _| {
(
state.geometry_window,
state.last_bounds.map(|b| b.size.width),
)
})
};
for _ in 0..3 {
draw_both(cx);
}
let settled = notifications.get();
for _ in 0..3 {
draw_both(cx);
}
assert_eq!(
notifications.get(),
settled,
"drawing both windows again must not notify"
);
let (owner, width) = measured(cx);
assert_eq!(owner, Some(wide.window_id()));
assert!(
width.is_some_and(|w| w > px(500.)),
"the active wide window measures"
);

// Activating the other window hands measurement over to it.
narrow
.update(cx, |_, window, _| window.activate_window())
.unwrap();
for _ in 0..3 {
draw_both(cx);
}
let (owner, width) = measured(cx);
assert_eq!(owner, Some(narrow.window_id()));
assert!(width.is_some_and(|w| w <= px(500.)));
}

#[gpui::test]
fn test_noop_scroll_notifies_diagnostic_dismissal(cx: &mut TestAppContext) {
use std::{cell::Cell, rc::Rc};
Expand Down
Loading
Loading