assistant2: Add scrollbar to active thread (#27534)
This required adding scrollbar support to `list`. Since `list` is virtualized, the scrollbar height will change as more items are measured. When the user manually drags the scrollbar, we'll persist the initial height and offset calculations accordingly to prevent the scrollbar from moving away from the cursor as new items are measured. We're not doing this yet, but in the future, it'd be nice to budget some time each frame to layout unmeasured items so that the scrollbar height is as accurate as possible. Release Notes: - N/A
This commit is contained in:
parent
0a3c8a6790
commit
7b40ab30d7
3 changed files with 189 additions and 15 deletions
|
@ -6,16 +6,15 @@ use crate::thread_store::ThreadStore;
|
|||
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
|
||||
use crate::ui::{ContextPill, ToolReadyPopUp, ToolReadyPopupEvent};
|
||||
use crate::AssistantPanel;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
linear_color_stop, linear_gradient, list, percentage, pulsating_between, AbsoluteLength,
|
||||
Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
||||
Entity, Focusable, Hsla, Length, ListAlignment, ListOffset, ListState, ScrollHandle,
|
||||
StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle,
|
||||
WeakEntity, WindowHandle,
|
||||
Entity, Focusable, Hsla, Length, ListAlignment, ListOffset, ListState, MouseButton,
|
||||
ScrollHandle, Stateful, StyleRefinement, Subscription, Task, TextStyleRefinement,
|
||||
Transformation, UnderlineStyle, WeakEntity, WindowHandle,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
|
@ -24,7 +23,7 @@ use settings::Settings as _;
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Tooltip};
|
||||
use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{OpenOptions, Workspace};
|
||||
|
||||
|
@ -39,6 +38,7 @@ pub struct ActiveThread {
|
|||
save_thread_task: Option<Task<()>>,
|
||||
messages: Vec<MessageId>,
|
||||
list_state: ListState,
|
||||
scrollbar_state: ScrollbarState,
|
||||
rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
|
||||
rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
|
||||
editing_message: Option<(MessageId, EditMessageState)>,
|
||||
|
@ -227,6 +227,14 @@ impl ActiveThread {
|
|||
cx.subscribe_in(&thread, window, Self::handle_thread_event),
|
||||
];
|
||||
|
||||
let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), {
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| this.render_message(ix, window, cx))
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
language_registry,
|
||||
thread_store,
|
||||
|
@ -239,13 +247,8 @@ impl ActiveThread {
|
|||
rendered_tool_use_labels: HashMap::default(),
|
||||
expanded_tool_uses: HashMap::default(),
|
||||
expanded_thinking_segments: HashMap::default(),
|
||||
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| this.render_message(ix, window, cx))
|
||||
.unwrap()
|
||||
}
|
||||
}),
|
||||
list_state: list_state.clone(),
|
||||
scrollbar_state: ScrollbarState::new(list_state),
|
||||
editing_message: None,
|
||||
last_error: None,
|
||||
pop_ups: Vec::new(),
|
||||
|
@ -1749,13 +1752,48 @@ impl ActiveThread {
|
|||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
|
||||
div()
|
||||
.occlude()
|
||||
.id("active-thread-scrollbar")
|
||||
.on_mouse_move(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|_, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_1()
|
||||
.bottom_0()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ActiveThread {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.relative()
|
||||
.child(list(self.list_state.clone()).flex_grow())
|
||||
.children(self.render_confirmations(cx))
|
||||
.child(self.render_vertical_scrollbar(cx))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,12 @@ impl List {
|
|||
#[derive(Clone)]
|
||||
pub struct ListState(Rc<RefCell<StateInner>>);
|
||||
|
||||
impl std::fmt::Debug for ListState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("ListState")
|
||||
}
|
||||
}
|
||||
|
||||
struct StateInner {
|
||||
last_layout_bounds: Option<Bounds<Pixels>>,
|
||||
last_padding: Option<Edges<Pixels>>,
|
||||
|
@ -57,6 +63,7 @@ struct StateInner {
|
|||
reset: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,
|
||||
scrollbar_drag_start_height: Option<Pixels>,
|
||||
}
|
||||
|
||||
/// Whether the list is scrolling from top to bottom or bottom to top.
|
||||
|
@ -198,6 +205,7 @@ impl ListState {
|
|||
overdraw,
|
||||
scroll_handler: None,
|
||||
reset: false,
|
||||
scrollbar_drag_start_height: None,
|
||||
})));
|
||||
this.splice(0..0, item_count);
|
||||
this
|
||||
|
@ -211,6 +219,7 @@ impl ListState {
|
|||
let state = &mut *self.0.borrow_mut();
|
||||
state.reset = true;
|
||||
state.logical_scroll_top = None;
|
||||
state.scrollbar_drag_start_height = None;
|
||||
state.items.summary().count
|
||||
};
|
||||
|
||||
|
@ -355,6 +364,62 @@ impl ListState {
|
|||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Call this method when the user starts dragging the scrollbar.
|
||||
///
|
||||
/// This will prevent the height reported to the scrollbar from changing during the drag
|
||||
/// as items in the overdraw get measured, and help offset scroll position changes accordingly.
|
||||
pub fn scrollbar_drag_started(&self) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.scrollbar_drag_start_height = Some(state.items.summary().height);
|
||||
}
|
||||
|
||||
/// Called when the user stops dragging the scrollbar.
|
||||
///
|
||||
/// See `scrollbar_drag_started`.
|
||||
pub fn scrollbar_drag_ended(&self) {
|
||||
self.0.borrow_mut().scrollbar_drag_start_height.take();
|
||||
}
|
||||
|
||||
/// Set the offset from the scrollbar
|
||||
pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
|
||||
self.0.borrow_mut().set_offset_from_scrollbar(point);
|
||||
}
|
||||
|
||||
/// Returns the size of items we have measured.
|
||||
/// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly.
|
||||
pub fn content_size_for_scrollbar(&self) -> Size<Pixels> {
|
||||
let state = self.0.borrow();
|
||||
let bounds = state.last_layout_bounds.unwrap_or_default();
|
||||
|
||||
let height = state
|
||||
.scrollbar_drag_start_height
|
||||
.unwrap_or_else(|| state.items.summary().height);
|
||||
|
||||
Size::new(bounds.size.width, height)
|
||||
}
|
||||
|
||||
/// Returns the current scroll offset adjusted for the scrollbar
|
||||
pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
|
||||
let state = &self.0.borrow();
|
||||
let logical_scroll_top = state.logical_scroll_top();
|
||||
|
||||
let mut cursor = state.items.cursor::<ListItemSummary>(&());
|
||||
let summary: ListItemSummary =
|
||||
cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right, &());
|
||||
let content_height = state.items.summary().height;
|
||||
let drag_offset =
|
||||
// if dragging the scrollbar, we want to offset the point if the height changed
|
||||
content_height - state.scrollbar_drag_start_height.unwrap_or(content_height);
|
||||
let offset = summary.height + logical_scroll_top.offset_in_item - drag_offset;
|
||||
|
||||
Point::new(px(0.), -offset)
|
||||
}
|
||||
|
||||
/// Return the bounds of the viewport in pixels.
|
||||
pub fn viewport_bounds(&self) -> Bounds<Pixels> {
|
||||
self.0.borrow().last_layout_bounds.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl StateInner {
|
||||
|
@ -695,6 +760,37 @@ impl StateInner {
|
|||
Ok(layout_response)
|
||||
})
|
||||
}
|
||||
|
||||
// Scrollbar support
|
||||
|
||||
fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
|
||||
let Some(bounds) = self.last_layout_bounds else {
|
||||
return;
|
||||
};
|
||||
let height = bounds.size.height;
|
||||
|
||||
let padding = self.last_padding.unwrap_or_default();
|
||||
let content_height = self.items.summary().height;
|
||||
let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
|
||||
let drag_offset =
|
||||
// if dragging the scrollbar, we want to offset the point if the height changed
|
||||
content_height - self.scrollbar_drag_start_height.unwrap_or(content_height);
|
||||
let new_scroll_top = (point.y - drag_offset).abs().max(px(0.)).min(scroll_max);
|
||||
|
||||
if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
|
||||
self.logical_scroll_top = None;
|
||||
} else {
|
||||
let mut cursor = self.items.cursor::<ListItemSummary>(&());
|
||||
cursor.seek(&Height(new_scroll_top), Bias::Right, &());
|
||||
|
||||
let item_ix = cursor.start().count;
|
||||
let offset_in_item = new_scroll_top - cursor.start().height;
|
||||
self.logical_scroll_top = Some(ListOffset {
|
||||
item_ix,
|
||||
offset_in_item,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ListItem {
|
||||
|
|
|
@ -4,8 +4,8 @@ use crate::{prelude::*, px, relative, IntoElement};
|
|||
use gpui::{
|
||||
point, quad, Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners,
|
||||
Edges, Element, ElementId, Entity, EntityId, GlobalElementId, Hitbox, Hsla, LayoutId,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollHandle, ScrollWheelEvent,
|
||||
Size, Style, UniformListScrollHandle, Window,
|
||||
ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollHandle,
|
||||
ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window,
|
||||
};
|
||||
|
||||
pub struct Scrollbar {
|
||||
|
@ -39,6 +39,39 @@ impl ScrollableHandle for UniformListScrollHandle {
|
|||
}
|
||||
}
|
||||
|
||||
impl ScrollableHandle for ListState {
|
||||
fn content_size(&self) -> Option<ContentSize> {
|
||||
Some(ContentSize {
|
||||
size: self.content_size_for_scrollbar(),
|
||||
scroll_adjustment: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_offset(&self, point: Point<Pixels>) {
|
||||
self.set_offset_from_scrollbar(point);
|
||||
}
|
||||
|
||||
fn offset(&self) -> Point<Pixels> {
|
||||
self.scroll_px_offset_for_scrollbar()
|
||||
}
|
||||
|
||||
fn drag_started(&self) {
|
||||
self.scrollbar_drag_started();
|
||||
}
|
||||
|
||||
fn drag_ended(&self) {
|
||||
self.scrollbar_drag_ended();
|
||||
}
|
||||
|
||||
fn viewport(&self) -> Bounds<Pixels> {
|
||||
self.viewport_bounds()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollableHandle for ScrollHandle {
|
||||
fn content_size(&self) -> Option<ContentSize> {
|
||||
let last_children_index = self.children_count().checked_sub(1)?;
|
||||
|
@ -92,6 +125,8 @@ pub trait ScrollableHandle: Debug + 'static {
|
|||
fn offset(&self) -> Point<Pixels>;
|
||||
fn viewport(&self) -> Bounds<Pixels>;
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn drag_started(&self) {}
|
||||
fn drag_ended(&self) {}
|
||||
}
|
||||
|
||||
/// A scrollbar state that should be persisted across frames.
|
||||
|
@ -300,6 +335,8 @@ impl Element for Scrollbar {
|
|||
return;
|
||||
}
|
||||
|
||||
scroll.drag_started();
|
||||
|
||||
if thumb_bounds.contains(&event.position) {
|
||||
let offset = event.position.along(axis) - thumb_bounds.origin.along(axis);
|
||||
state.drag.set(Some(offset));
|
||||
|
@ -349,7 +386,7 @@ impl Element for Scrollbar {
|
|||
});
|
||||
let state = self.state.clone();
|
||||
let axis = self.kind;
|
||||
window.on_mouse_event(move |event: &MouseMoveEvent, _, _, cx| {
|
||||
window.on_mouse_event(move |event: &MouseMoveEvent, _, window, cx| {
|
||||
if let Some(drag_state) = state.drag.get().filter(|_| event.dragging()) {
|
||||
if let Some(ContentSize {
|
||||
size: item_size, ..
|
||||
|
@ -381,6 +418,7 @@ impl Element for Scrollbar {
|
|||
scroll.set_offset(point(scroll.offset().x, drag_offset));
|
||||
}
|
||||
};
|
||||
window.refresh();
|
||||
if let Some(id) = state.parent_id {
|
||||
cx.notify(id);
|
||||
}
|
||||
|
@ -390,9 +428,11 @@ impl Element for Scrollbar {
|
|||
}
|
||||
});
|
||||
let state = self.state.clone();
|
||||
let scroll = self.state.scroll_handle.clone();
|
||||
window.on_mouse_event(move |_event: &MouseUpEvent, phase, _, cx| {
|
||||
if phase.bubble() {
|
||||
state.drag.take();
|
||||
scroll.drag_ended();
|
||||
if let Some(id) = state.parent_id {
|
||||
cx.notify(id);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue