@@ -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);
}