image_viewer: Make the outline panel width adjustable

- Add a drag handle on the outline panel's right edge that resizes the
  panel between 120 and 480 pixels
- Track the drag on the surrounding row so the pointer can roam, and
  clamp the width to keep the panel usable
- Keep the chosen width while toggling the panel and inherit it when
  the view is split
This commit is contained in:
yenru0 committed 2026-09-25 01:56:32 +09:00
1 parent 1b2685ac9b
commit de4affc638
1 file changed
+94 -12
+94 -12
View File
@@ -12,12 +12,12 @@ use editor::{
};
use file_icons::FileIcons;
use gpui::{
AnyElement, App, Bounds, Context, DismissEvent, DispatchPhase, Element, ElementId, Entity,
EventEmitter, FocusHandle, Focusable, Font, GlobalElementId, InspectorElementId,
InteractiveElement, IntoElement, KeyDownEvent, LayoutId, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, ParentElement, PinchEvent, Pixels, Point, Render, RenderImage,
ScrollDelta, ScrollWheelEvent, Style, Styled, Subscription, Task, WeakEntity, Window, actions,
anchored, canvas, checkerboard, deferred, div, img, point, px, size,
AnyElement, App, Bounds, Context, DismissEvent, DispatchPhase, DragMoveEvent, Element,
ElementId, Entity, EventEmitter, FocusHandle, Focusable, Font, GlobalElementId,
InspectorElementId, InteractiveElement, IntoElement, KeyDownEvent, LayoutId, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, PinchEvent, Pixels, Point, Render,
RenderImage, ScrollDelta, ScrollWheelEvent, Style, Styled, Subscription, Task, WeakEntity,
Window, actions, anchored, canvas, checkerboard, deferred, div, img, point, px, size,
};
use language::File as _;
use pdf_viewer::{
@@ -123,6 +123,9 @@ const FIND_BAR_WIDTH: f32 = 280.0;
const THUMBNAIL_DPI: f32 = 20.0; // Renders letter pages about 170px wide.
const THUMBNAIL_PANEL_WIDTH: f32 = 150.0;
const OUTLINE_PANEL_WIDTH: f32 = 220.0;
const OUTLINE_PANEL_MIN_WIDTH: f32 = 120.0;
const OUTLINE_PANEL_MAX_WIDTH: f32 = 480.0;
const OUTLINE_RESIZE_HANDLE_WIDTH: f32 = 6.0;
const OUTLINE_INDENT: f32 = 12.0;
const PROPERTIES_PANEL_WIDTH: f32 = 220.0;
const HIGHLIGHT_COLORS: [pdf_viewer::PdfAnnotationColor; 4] = [
@@ -202,6 +205,15 @@ struct NormRect {
height: f32,
}
/// Drag payload identifying a resize drag of the outline panel's right edge.
struct OutlinePanelResize;
impl Render for OutlinePanelResize {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
}
}
/// Which annotation tool the viewer is armed with.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PdfAnnotationTool {
@@ -267,6 +279,10 @@ pub struct ImageView {
pdf_find: Option<PdfFindState>,
show_thumbnail_panel: bool,
show_outline_panel: bool,
/// Current outline panel width, adjusted by dragging its resize handle.
outline_panel_width: Pixels,
/// Mouse x and panel width when the outline resize drag started.
outline_resize_origin: Option<(Pixels, Pixels)>,
show_properties_panel: bool,
presentation_mode: bool,
page_rotation: u16,
@@ -425,6 +441,8 @@ impl ImageView {
pdf_find: None,
show_thumbnail_panel: false,
show_outline_panel: false,
outline_panel_width: px(OUTLINE_PANEL_WIDTH),
outline_resize_origin: None,
show_properties_panel: false,
presentation_mode: false,
page_rotation: 0,
@@ -1911,6 +1929,7 @@ impl ImageView {
/// Renders the outline sidebar, indenting nested entries like a document tree.
fn render_outline_panel(
outline: Option<&[pdf_viewer::PdfOutlineItem]>,
width: Pixels,
cx: &mut Context<Self>,
) -> AnyElement {
let mut next_id = 0_usize;
@@ -1921,7 +1940,8 @@ impl ImageView {
div()
.id("pdf-outline")
.w(px(OUTLINE_PANEL_WIDTH))
.debug_selector(|| "pdf-outline".into())
.w(width)
.h_full()
.p_2()
.overflow_y_scroll()
@@ -1937,6 +1957,37 @@ impl ImageView {
.into_any_element()
}
/// Renders the drag handle on the outline panel's right edge. The actual
/// resize tracking happens in the `on_drag_move` handler on the surrounding
/// row so the pointer can roam while dragging.
fn render_outline_resize_handle(&mut self, cx: &mut Context<Self>) -> AnyElement {
div()
.id("outline-resize-handle")
.debug_selector(|| "outline-resize-handle".into())
.occlude()
.w(px(OUTLINE_RESIZE_HANDLE_WIDTH))
.h_full()
.cursor_col_resize()
.hover(|style| style.bg(cx.theme().colors().element_hover))
.on_drag(OutlinePanelResize, |_, _, _, cx| {
cx.stop_propagation();
cx.new(|_| OutlinePanelResize)
})
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, event: &MouseDownEvent, _, _| {
this.outline_resize_origin = Some((event.position.x, this.outline_panel_width));
}),
)
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _: &MouseUpEvent, _, _| {
this.outline_resize_origin = None;
}),
)
.into_any_element()
}
fn render_outline_items(
items: &[pdf_viewer::PdfOutlineItem],
depth: usize,
@@ -3147,6 +3198,8 @@ impl Item for ImageView {
pdf_find: None,
show_thumbnail_panel: false,
show_outline_panel: false,
outline_panel_width: self.outline_panel_width,
outline_resize_origin: None,
show_properties_panel: false,
presentation_mode: false,
page_rotation: 0,
@@ -3934,16 +3987,46 @@ impl Render for ImageView {
cx,
))
} else if self.show_outline_panel {
Some(Self::render_outline_panel(self.pdf_outline.as_deref(), cx))
Some(Self::render_outline_panel(
self.pdf_outline.as_deref(),
self.outline_panel_width,
cx,
))
} else {
None
};
let has_sidebar = sidebar.is_some();
let resize_handle = if self.show_outline_panel {
Some(self.render_outline_resize_handle(cx))
} else {
None
};
h_flex()
.size_full()
.when_some(sidebar, |this, sidebar| {
this.child(sidebar).child(Divider::vertical())
})
.when_some(sidebar, |this, sidebar| this.child(sidebar))
.when_some(resize_handle, |this, handle| this.child(handle))
.when(has_sidebar, |this| this.child(Divider::vertical()))
.on_drag_move(cx.listener(
|this, event: &DragMoveEvent<OutlinePanelResize>, _, cx| {
if let Some((origin_x, origin_width)) =
this.outline_resize_origin
{
let width =
origin_width + (event.event.position.x - origin_x);
this.outline_panel_width = if width < px(OUTLINE_PANEL_MIN_WIDTH) {
px(OUTLINE_PANEL_MIN_WIDTH)
} else if width > px(OUTLINE_PANEL_MAX_WIDTH) {
px(OUTLINE_PANEL_MAX_WIDTH)
} else {
width
};
cx.stop_propagation();
cx.notify();
}
},
))
.child(div().flex_1().h_full().child(main_pdf_view))
.when(self.show_text_panel, |this| {
this.child(Divider::vertical()).child(
@@ -5301,7 +5384,6 @@ mod tests {
assert_eq!(cache.current_memory_bytes(), 0);
assert!(cache.is_empty());
}
}
mod persistence {