Merge pull request #462 from pop-os/scroll-fixes

Fix horizontal scrolling with wheel or touchpad and improve scrollbar behavior
This commit is contained in:
Jeremy Soller 2025-11-12 10:14:00 -07:00 committed by GitHub
commit e66aa263a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -324,7 +324,7 @@ where
) -> mouse::Interaction { ) -> mouse::Interaction {
let state = tree.state.downcast_ref::<State>(); let state = tree.state.downcast_ref::<State>();
if let Some(Dragging::ScrollbarV { .. }) = &state.dragging { if let Some(Dragging::ScrollbarV { .. } | Dragging::ScrollbarH { .. }) = &state.dragging {
return mouse::Interaction::Idle; return mouse::Interaction::Idle;
} }
@ -367,12 +367,12 @@ where
let mut editor = self.editor.lock().unwrap(); let mut editor = self.editor.lock().unwrap();
let cosmic_theme = theme.cosmic(); let cosmic_theme = theme.cosmic();
let scrollbar_w = cosmic_theme.spacing.space_xxs as i32; let scrollbar_size = cosmic_theme.spacing.space_xxs as i32;
let view_position = layout.position() + [self.padding.left, self.padding.top].into(); let view_position = layout.position() + [self.padding.left, self.padding.top].into();
let view_w = cmp::min(viewport.width as i32, layout.bounds().width as i32) let view_w = cmp::min(viewport.width as i32, layout.bounds().width as i32)
- self.padding.horizontal() as i32 - self.padding.horizontal() as i32
- scrollbar_w; - scrollbar_size;
let view_h = cmp::min(viewport.height as i32, layout.bounds().height as i32) let view_h = cmp::min(viewport.height as i32, layout.bounds().height as i32)
- self.padding.vertical() as i32; - self.padding.vertical() as i32;
@ -597,7 +597,7 @@ where
let rect = Rectangle::new( let rect = Rectangle::new(
[image_w as f32 / scale_factor, start_y as f32 / scale_factor].into(), [image_w as f32 / scale_factor, start_y as f32 / scale_factor].into(),
Size::new( Size::new(
scrollbar_w as f32, scrollbar_size as f32,
(end_y as f32 - start_y as f32) / scale_factor, (end_y as f32 - start_y as f32) / scale_factor,
), ),
); );
@ -606,17 +606,17 @@ where
let (buffer_w_opt, buffer_h_opt) = buffer.size(); let (buffer_w_opt, buffer_h_opt) = buffer.size();
let buffer_w = buffer_w_opt.unwrap_or(0.0); let buffer_w = buffer_w_opt.unwrap_or(0.0);
let buffer_h = buffer_h_opt.unwrap_or(0.0); let buffer_h = buffer_h_opt.unwrap_or(0.0);
let scrollbar_h_width = image_w as f32 / scale_factor - scrollbar_w as f32; let scrollbar_h_width = (image_w as f32) / scale_factor;
if buffer_w < max_line_width { if buffer_w < max_line_width {
let rect = Rectangle::new( let rect = Rectangle::new(
[ [
(buffer.scroll().horizontal / max_line_width) * scrollbar_h_width, (buffer.scroll().horizontal / max_line_width) * scrollbar_h_width,
buffer_h / scale_factor - scrollbar_w as f32, buffer_h / scale_factor - scrollbar_size as f32,
] ]
.into(), .into(),
Size::new( Size::new(
(buffer_w / max_line_width) * scrollbar_h_width, (buffer_w / max_line_width) * scrollbar_h_width,
scrollbar_w as f32, scrollbar_size as f32,
), ),
); );
state.scrollbar_h_rect.set(Some(rect)); state.scrollbar_h_rect.set(Some(rect));
@ -643,88 +643,82 @@ where
// Draw editor UI // Draw editor UI
renderer.with_translation(Vector::new(view_position.x, view_position.y), |renderer| { renderer.with_translation(Vector::new(view_position.x, view_position.y), |renderer| {
renderer.with_transformation(Transformation::scale(1.0 / scale_factor), |renderer| { renderer.with_transformation(Transformation::scale(1.0 / scale_factor), |renderer| {
renderer.with_layer( // Draw cached image (only has line numbers)
Rectangle::new( if let Some(ref handle) = *handle_opt {
Point::new(0.0, 0.0), let image_size = image::Renderer::measure_image(renderer, handle);
Size::new(image_w as f32, image_h as f32), image::Renderer::draw_image(
), renderer,
|renderer| { handle.clone(),
// Draw cached image (only has line numbers) image::FilterMethod::Nearest,
if let Some(ref handle) = *handle_opt { Rectangle::new(
let image_size = image::Renderer::measure_image(renderer, handle); Point::new(0.0, 0.0),
image::Renderer::draw_image( Size::new(image_size.width as f32, image_size.height as f32),
renderer, ),
handle.clone(), Radians(0.0),
image::FilterMethod::Nearest, 1.0,
Rectangle::new( [0.0; 4],
Point::new(0.0, 0.0), );
Size::new(image_size.width as f32, image_size.height as f32), }
),
Radians(0.0),
1.0,
[0.0; 4],
);
}
// Calculate editor position // Calculate editor position
let scroll_x = editor.with_buffer(|buffer| buffer.scroll().horizontal); let scroll_x = editor.with_buffer(|buffer| buffer.scroll().horizontal);
let pos = Point::new(editor_offset_x as f32, 0.0); let pos = Point::new(editor_offset_x as f32 - scroll_x, 0.0);
let size = Size::new((image_w - editor_offset_x) as f32, image_h as f32); let size = Size::new((image_w - editor_offset_x) as f32, image_h as f32);
let clip_bounds = Rectangle::new(Point::new(editor_offset_x as f32, 0.0), size);
renderer.with_layer(clip_bounds, |renderer| {
// Create custom renderer for rectangles
let mut custom_renderer = CustomRenderer { renderer, pos };
// Create custom renderer for rectangles // Draw line highlight
let mut custom_renderer = CustomRenderer { renderer, pos }; if self.highlight_current_line {
let line_highlight = {
// Draw line highlight let convert_color = |color: syntect::highlighting::Color| {
if self.highlight_current_line { cosmic_text::Color::rgba(color.r, color.g, color.b, color.a)
let line_highlight = {
let convert_color = |color: syntect::highlighting::Color| {
cosmic_text::Color::rgba(color.r, color.g, color.b, color.a)
};
let syntax_theme = editor.theme();
//TODO: ideal fallback for line highlight color
syntax_theme
.settings
.line_highlight
.map_or(editor.background_color(), convert_color)
}; };
let syntax_theme = editor.theme();
//TODO: ideal fallback for line highlight color
syntax_theme
.settings
.line_highlight
.map_or(editor.background_color(), convert_color)
};
let cursor = editor.cursor(); let cursor = editor.cursor();
editor.with_buffer(|buffer| { editor.with_buffer(|buffer| {
for run in buffer.layout_runs() { for run in buffer.layout_runs() {
if run.line_i != cursor.line { if run.line_i != cursor.line {
continue; continue;
}
custom_renderer.rectangle(
0,
run.line_top as i32,
(image_w - editor_offset_x) as u32,
metrics.line_height as u32,
line_highlight,
);
} }
custom_renderer.rectangle(
0,
run.line_top as i32,
(image_w - editor_offset_x) as u32,
metrics.line_height as u32,
line_highlight,
);
}
});
}
// Draw editor selection, cursor, etc.
editor.render(&mut custom_renderer);
// Draw editor text
match editor.buffer_ref() {
cosmic_text::BufferRef::Arc(buffer) => {
renderer.fill_raw(Raw {
buffer: Arc::downgrade(&buffer),
position: pos,
color: Color::new(1.0, 1.0, 1.0, 1.0),
clip_bounds,
}); });
} }
_ => {
// Draw editor selection, cursor, etc. log::error!("cosmic-text buffer not an Arc");
editor.render(&mut custom_renderer);
// Draw editor text
match editor.buffer_ref() {
cosmic_text::BufferRef::Arc(buffer) => {
renderer.fill_raw(Raw {
buffer: Arc::downgrade(&buffer),
position: pos - Vector::new(scroll_x, 0.0),
color: Color::new(1.0, 1.0, 1.0, 1.0),
clip_bounds: Rectangle::new(pos, size),
});
}
_ => {
log::error!("cosmic-text buffer not an Arc");
}
} }
}, }
) })
}) })
}); });
@ -820,7 +814,7 @@ where
// Draw horizontal scrollbar // Draw horizontal scrollbar
//TODO: reduce repitition //TODO: reduce repitition
if let Some(scrollbar_h_rect) = state.scrollbar_h_rect.get() { if let Some(scrollbar_h_rect) = state.scrollbar_h_rect.get() {
/*TODO: horizontal scrollbar track? /*
// neutral_3, 0.7 // neutral_3, 0.7
let track_color = cosmic_theme let track_color = cosmic_theme
.palette .palette
@ -832,9 +826,12 @@ where
renderer.fill_quad( renderer.fill_quad(
Quad { Quad {
bounds: Rectangle::new( bounds: Rectangle::new(
Point::new(image_position.x, image_position.y + scrollbar_h_rect.y), Point::new(
image_position.x + scrollbar_h_rect.x,
image_position.y + scrollbar_h_rect.y,
),
Size::new( Size::new(
layout.bounds().width - scrollbar_w as f32, layout.bounds().width - scrollbar_h_rect.x - scrollbar_size as f32,
scrollbar_h_rect.height, scrollbar_h_rect.height,
), ),
), ),
@ -1095,7 +1092,10 @@ where
// Do this first as the horizontal scrollbar is on top of the buffer // Do this first as the horizontal scrollbar is on top of the buffer
if let Some(scrollbar_h_rect) = state.scrollbar_h_rect.get() { if let Some(scrollbar_h_rect) = state.scrollbar_h_rect.get() {
if scrollbar_h_rect.contains(Point::new(x_logical, y_logical)) { if scrollbar_h_rect.contains(Point::new(x_logical, y_logical)) {
state.dragging = Some(Dragging::ScrollbarH { start_x: x }); state.dragging = Some(Dragging::ScrollbarH {
start_x: x,
start_scroll: editor.with_buffer(|buffer| buffer.scroll()),
});
} }
} }
@ -1232,7 +1232,10 @@ where
buffer.set_scroll(scroll); buffer.set_scroll(scroll);
}); });
} }
Dragging::ScrollbarH { start_x } => { Dragging::ScrollbarH {
start_x,
start_scroll,
} => {
editor.with_buffer_mut(|buffer| { editor.with_buffer_mut(|buffer| {
//TODO: store this in state? //TODO: store this in state?
let mut max_line_width = 0.0; let mut max_line_width = 0.0;
@ -1244,10 +1247,10 @@ where
let buffer_w = buffer.size().0.unwrap_or(0.0); let buffer_w = buffer.size().0.unwrap_or(0.0);
let mut scroll = buffer.scroll(); let mut scroll = buffer.scroll();
scroll.horizontal = (((x - start_x) / buffer_w) let scroll_offset = ((x - start_x) / buffer_w) * max_line_width;
* max_line_width) scroll.horizontal = (start_scroll.horizontal + scroll_offset)
.max(0.0) .min(max_line_width - buffer_w)
.min(max_line_width - buffer_w); .max(0.0);
buffer.set_scroll(scroll); buffer.set_scroll(scroll);
}); });
} }
@ -1258,15 +1261,33 @@ where
} }
Event::Mouse(MouseEvent::WheelScrolled { delta }) => { Event::Mouse(MouseEvent::WheelScrolled { delta }) => {
if let Some(_p) = cursor_position.position_in(layout.bounds()) { if let Some(_p) = cursor_position.position_in(layout.bounds()) {
let pixels = match delta { let (mut x, mut y) = match delta {
ScrollDelta::Lines { x: _, y } => { ScrollDelta::Lines { x, y } => {
//TODO: this adjustment is just a guess! //TODO: this adjustment is just a guess!
let metrics = editor.with_buffer(|buffer| buffer.metrics()); let metrics = editor.with_buffer(|buffer| buffer.metrics());
-y * metrics.line_height (-x * metrics.line_height, -y * metrics.line_height)
} }
ScrollDelta::Pixels { x: _, y } => -y, ScrollDelta::Pixels { x, y } => (-x, -y),
} * 4.0; };
editor.action(Action::Scroll { pixels }); x *= 4.0;
y *= 4.0;
editor.action(Action::Scroll { pixels: y });
editor.with_buffer_mut(|buffer| {
//TODO: store this in state?
let mut max_line_width = 0.0;
for run in buffer.layout_runs() {
if run.line_w > max_line_width {
max_line_width = run.line_w;
}
}
let buffer_w = buffer.size().0.unwrap_or(0.0);
let mut scroll = buffer.scroll();
scroll.horizontal = (scroll.horizontal + x)
.min(max_line_width - buffer_w)
.max(0.0);
buffer.set_scroll(scroll);
});
status = Status::Captured; status = Status::Captured;
} }
} }
@ -1302,10 +1323,11 @@ enum ClickKind {
Triple, Triple,
} }
#[derive(Debug)]
enum Dragging { enum Dragging {
Buffer, Buffer,
ScrollbarV { start_y: f32, start_scroll: Scroll }, ScrollbarV { start_y: f32, start_scroll: Scroll },
ScrollbarH { start_x: f32 }, ScrollbarH { start_x: f32, start_scroll: Scroll },
} }
pub struct State { pub struct State {