Files
galaxy/app/src/code/editor/element.rs
T

1695 lines
66 KiB
Rust

mod gutter_button;
use std::ops::Range;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
pub use gutter_button::{AddAsContextButton, CommentButton, RevertHunkButton};
use parking_lot::Mutex;
use pathfinder_color::ColorU;
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::{vec2f, Vector2F};
use galaxy_core::features::FeatureFlag;
use galaxy_core::ui::appearance::Appearance;
use galaxy_core::ui::theme::color::internal_colors;
use galaxy_core::ui::theme::Fill;
use galaxy_editor::editor::EditorView;
use galaxy_editor::render::element::lens_element::RichTextElementLens;
use galaxy_editor::render::element::{RenderableBlock, RichTextElement, VerticalExpansionBehavior};
use galaxy_editor::render::model::{
gutter_expansion_button_types, BlockLocation, ExpansionType, LineCount, RenderState,
};
use galaxyui::elements::new_scrollable::{NewScrollableElement, ScrollableAxis};
use galaxyui::elements::{
Align, Axis, Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, Empty, F32Ext, Flex,
Hoverable, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement,
ParentOffsetBounds, Point, Radius, ScrollData, Stack, Text, ZIndex,
};
use galaxyui::event::DispatchedEvent;
use galaxyui::fonts::FamilyId;
use galaxyui::ui_components::components::UiComponent;
use galaxyui::units::{IntoPixels, Pixels};
use galaxyui::{
AfterLayoutContext, AppContext, ClipBounds, Element, Event, EventContext, LayoutContext,
ModelHandle, PaintContext, SingletonEntity, SizeConstraint,
};
use pathfinder_geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
};
use super::diff::{DiffHunkDisplay, DiffStatus};
use super::model::DiffNavigationState;
use crate::code::editor::element::gutter_button::GutterButton;
use crate::code::editor::line::EditorLineLocation;
use crate::code::editor::view::{CodeEditorViewAction, SavedComment};
use crate::settings::CodeEditorLineNumberMode;
use crate::view_components::action_button::{ActionButtonTheme, SecondaryTheme};
pub const GUTTER_WIDTH: f32 = 94.;
const VERTICAL_DIFF_HUNK_INDICATOR_WIDTH: f32 = 3.;
const VERTICAL_DIFF_HUNK_INDICATOR_HOVERED_WIDTH: f32 = 8.;
fn highlight_element(appearance: &Appearance) -> Box<dyn Element> {
let border_color = appearance.theme().accent();
Container::new(Empty::new().finish())
.with_border(Border::all(2.).with_border_fill(border_color))
.finish()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeType {
Add,
Remove,
}
#[derive(Debug, Clone, Copy)]
pub enum GutterElementType {
DiffHunk {
hunk: Option<DiffHunkDisplay>,
change_type: ChangeType,
},
HiddenSection {
expansion_type: ExpansionType,
},
}
/// The inner editor element wrapped by the EditorWrapper. Currently we support a full scrollable
/// editor or a lens element into a section of the buffer.
pub enum InnerEditor<V: EditorView> {
FullEditor(RichTextElement<V>),
Lens(RichTextElementLens<V>),
}
impl<V: EditorView> InnerEditor<V> {
fn blocks(&self) -> Option<&[Box<dyn RenderableBlock>]> {
match self {
InnerEditor::FullEditor(element) => element.blocks(),
InnerEditor::Lens(element) => element.blocks(),
}
}
fn model(&self) -> &ModelHandle<RenderState> {
match self {
InnerEditor::FullEditor(element) => &element.model,
InnerEditor::Lens(element) => &element.model,
}
}
}
impl<V: EditorView> Element for InnerEditor<V> {
fn layout(
&mut self,
constraint: SizeConstraint,
ctx: &mut LayoutContext,
app: &AppContext,
) -> Vector2F {
match self {
InnerEditor::FullEditor(element) => element.layout(constraint, ctx, app),
InnerEditor::Lens(element) => element.layout(constraint, ctx, app),
}
}
fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) {
match self {
InnerEditor::FullEditor(element) => element.after_layout(ctx, app),
InnerEditor::Lens(element) => element.after_layout(ctx, app),
}
}
fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
match self {
InnerEditor::FullEditor(element) => element.paint(origin, ctx, app),
InnerEditor::Lens(element) => element.paint(origin, ctx, app),
};
}
fn size(&self) -> Option<Vector2F> {
None
}
fn origin(&self) -> Option<Point> {
None
}
fn dispatch_event(
&mut self,
event: &DispatchedEvent,
ctx: &mut EventContext,
app: &AppContext,
) -> bool {
match self {
InnerEditor::FullEditor(element) => element.dispatch_event(event, ctx, app),
InnerEditor::Lens(element) => element.dispatch_event(event, ctx, app),
}
}
}
struct GutterElement {
element: Box<dyn Element>,
offset: Pixels,
height: f32,
hovered: bool,
line: EditorLineLocation,
element_type: GutterElementType,
/// Optional background fill for removed-line (temporary) blocks.
overlay: Option<Fill>,
}
impl GutterElement {
/// Checks if the given position falls within this gutter element's bounds.
/// Returns a GutterRange if the position is contained, None otherwise.
/// When `check_y_axis_only` is true, a position is considered within the element
/// if it's anywhere within the `EditorWrapper`'s full line width.
fn contains_position(
&self,
position: Vector2F,
wrapper_origin: Vector2F,
wrapper_size: Vector2F,
check_y_axis_only: bool,
) -> Option<GutterRange> {
let gutter_origin = wrapper_origin + vec2f(0., self.offset.as_f32());
match self.element_type {
GutterElementType::HiddenSection { expansion_type } => {
let does_contain = if check_y_axis_only {
// We can count the position if it's within the editor wrapper's whole line width
let line_origin = Vector2F::new(wrapper_origin.x(), gutter_origin.y());
let line_size = Vector2F::new(wrapper_size.x(), self.height);
RectF::new(line_origin, line_size).contains_point(position)
} else {
// Hidden sections use the full gutter width
let size = vec2f(GUTTER_WIDTH, self.height);
RectF::new(gutter_origin, size).contains_point(position)
};
if does_contain {
Some(GutterRange::HiddenSection {
line: self.line.clone(),
expansion_type,
})
} else {
None
}
}
GutterElementType::DiffHunk { .. } => {
let does_contain = if check_y_axis_only {
// We can count the position if it's within the editor wrapper's whole line width
let line_origin = Vector2F::new(wrapper_origin.x(), gutter_origin.y());
let line_size = Vector2F::new(wrapper_size.x(), self.height);
RectF::new(line_origin, line_size).contains_point(position)
} else {
// For diff hunks, check if position is in the sliver or the full gutter
// Get the sliver size (always use expanded=true for hit testing)
let sliver_size = self.diff_hunk_size(true)?;
let sliver_rect = RectF::new(gutter_origin, sliver_size);
sliver_rect.contains_point(position)
};
if does_contain {
// Position is in the sliver
Some(GutterRange::DiffHunk {
line: self.line.clone(),
in_sliver: true,
})
} else {
// Check if position is in the full gutter width (but not in sliver)
let full_gutter_rect =
RectF::new(gutter_origin, vec2f(GUTTER_WIDTH, self.height));
if full_gutter_rect.contains_point(position) {
Some(GutterRange::DiffHunk {
line: self.line.clone(),
in_sliver: false,
})
} else {
None
}
}
}
}
}
/// The size of the diff hunk element in gutter (if exists).
fn diff_hunk_size(&self, gutter_element_is_hovered: bool) -> Option<Vector2F> {
match self.element_type {
GutterElementType::HiddenSection { .. } => {
// Hidden section elements are always horizontal.
Some(vec2f(GUTTER_WIDTH, self.height))
}
GutterElementType::DiffHunk { hunk: ref diff, .. } => {
let vertical_indicator_width = if gutter_element_is_hovered {
VERTICAL_DIFF_HUNK_INDICATOR_HOVERED_WIDTH
} else {
VERTICAL_DIFF_HUNK_INDICATOR_WIDTH
};
diff.as_ref()
.map(|_| vec2f(vertical_indicator_width, self.height))
}
}
}
// The color of the diff hunk indicator element in gutter (if it exists).
fn diff_indicator_color(&self, diff_hunks_are_expanded: bool) -> Option<ColorU> {
if let GutterElementType::DiffHunk {
hunk: Some(diff_hunk),
change_type,
} = self.element_type
{
match &diff_hunk {
DiffHunkDisplay::Add(color) | DiffHunkDisplay::Remove(color) => Some(*color),
DiffHunkDisplay::Replacement {
collapsed_color: change_color,
add_color,
remove_color,
} => {
if diff_hunks_are_expanded {
match change_type {
ChangeType::Add => Some(*add_color),
ChangeType::Remove => Some(*remove_color),
}
} else {
Some(*change_color)
}
}
}
} else {
None
}
}
}
/// States that need to live in between frames.
#[derive(Default)]
pub struct EditorWrapperState {
/// The line range of the hovered diff hunk.
hovered_diff_hunk: Mutex<Option<EditorLineLocation>>,
/// Whether there is an active click.
in_click: AtomicBool,
/// Mouse state handle for the plus button.
add_as_context_mouse_state: MouseStateHandle,
/// Mouse state handle for the revert button.
revert_mouse_state: MouseStateHandle,
/// Mouse state handle for the comment button.
comment_mouse_state: MouseStateHandle,
/// Tracks the line range where the add context button was last clicked,
/// so we don't show the button again until a different range is hovered.
last_clicked_range: Mutex<Option<Range<LineCount>>>,
}
impl EditorWrapperState {
/// Record that a range has been clicked (add context button was used)
pub fn record_clicked_range(&self, range: Range<LineCount>) {
*self.last_clicked_range.lock() = Some(range);
}
/// Clear the clicked range (when hovering over a new range)
pub fn clear_clicked_range(&self) {
*self.last_clicked_range.lock() = None;
}
/// Check if a range has been clicked
pub fn is_range_clicked(&self, range: &Range<LineCount>) -> bool {
self.last_clicked_range.lock().as_ref() == Some(range)
}
}
pub type EditorWrapperStateHandle = Arc<EditorWrapperState>;
pub enum GutterRange {
DiffHunk {
line: EditorLineLocation,
// Whether the position is in the sliver (the colored part) of the diff hunk element.
in_sliver: bool,
},
HiddenSection {
line: EditorLineLocation,
expansion_type: ExpansionType,
},
}
impl GutterRange {
pub fn line(&self) -> &EditorLineLocation {
match self {
GutterRange::DiffHunk { line, .. } => line,
GutterRange::HiddenSection { line, .. } => line,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum GutterHoverTarget {
// The entire line covered by the gutter is considered the hover target.
Line,
// Only the gutter element itself is considered the hover target.
GutterElement,
}
/// The caller defined handler for a click event.
type EditorWrapperClickHandler = Box<dyn FnMut(GutterRange, &mut EventContext)>;
/// UI Config for rendering line number.
pub struct LineNumberConfig {
pub font_family: FamilyId,
pub font_size: f32,
pub text_color: ColorU,
pub highlight_text_color: ColorU,
pub starting_line_number: Option<usize>,
pub mode: CodeEditorLineNumberMode,
pub active_line_number: Option<LineCount>,
pub active_cursor_is_visible: bool,
}
impl LineNumberConfig {
pub fn absolute_line_number(&self, line_count: LineCount) -> usize {
line_count.as_usize() + self.starting_line_number.unwrap_or(1)
}
pub fn display_line_number(&self, line_count: LineCount) -> usize {
if self.mode == CodeEditorLineNumberMode::Relative {
if let Some(active_line_number) = self.active_line_number {
if active_line_number != line_count {
return active_line_number
.as_usize()
.abs_diff(line_count.as_usize());
}
}
}
self.absolute_line_number(line_count)
}
}
struct CommentBox {
line_highlight_element: Box<dyn Element>,
line: EditorLineLocation,
}
pub struct EditorWrapper<V: EditorView> {
editor: InnerEditor<V>,
element_size: Option<Vector2F>,
element_origin: Option<Point>,
/// Whether the editor should expand vertically to fill the available space.
vertical_expansion_behavior: VerticalExpansionBehavior,
/// If there is no [`LineNumberConfig`], the entire left gutter won't be rendered.
line_number_config: Option<LineNumberConfig>,
gutter_elements: Option<Vec<GutterElement>>,
diff_status: DiffStatus,
state_handle: EditorWrapperStateHandle,
click_handler: EditorWrapperClickHandler,
/// The current state of diff navigation in the editor
diff_navigation_state: DiffNavigationState,
/// The line range of the focused diff hunk (if any)
focused_diff_line_range: Option<Range<LineCount>>,
should_handle_scroll_wheel: bool,
/// This helps us handle events properly on stacks. A stack will always
/// put its children on higher z-indexes than its origin, so a hit test using the standard
/// `z_index` method would always result in the event being covered (by the children of the
/// stack). Instead, we track the upper-bound of z-indexes _contained by_ the child element.
/// Then we use that upper bound to do the hit testing, which means a parent will always get
/// events from its children, regardless of whether they are stacks or not.
child_max_z_index: Option<ZIndex>,
/// Display state of the "add as agent context" button shown next to diff hunks.
add_hunk_as_context_button: Option<AddAsContextButton>,
/// Display state of the "revert" button shown next to diff hunks.
revert_hunk_button: Option<RevertHunkButton>,
/// Display state of the "comment" button shown next to diff hunks.
comment_button: Option<CommentButton>,
// Todo: kc combine all comment related fields into a struct.
comment_box: Option<CommentBox>,
/// Lines with saved comments attached. These lines always have an
/// indicator in the gutter element.
saved_comments: Vec<SavedComment>,
gutter_element_hover_target: GutterHoverTarget,
expand_diff_indicator_width_on_hover: bool,
comment_save_position_id: String,
find_references_save_position_id: String,
/// The line where find references card is anchored (if active).
find_references_anchor: Option<EditorLineLocation>,
}
impl<V: EditorView> EditorWrapper<V> {
fn paint_removed_line_overlays(
&self,
origin: Vector2F,
wrapper_size: Vector2F,
ctx: &mut PaintContext,
) {
// Removed lines: group consecutive Remove-type gutter elements by line_range and
// draw one full-width rect per group.
let Some(gutter_elements) = &self.gutter_elements else {
return;
};
struct Group {
start_y: f32,
end_y: f32,
line_range: Range<LineCount>,
overlay: Fill,
}
let mut group: Option<Group> = None;
let mut flush = |group: &mut Option<Group>| {
let Some(group) = group.take() else {
return;
};
ctx.scene
.draw_rect_without_hit_recording(RectF::new(
origin + vec2f(0., group.start_y),
vec2f(wrapper_size.x(), group.end_y - group.start_y),
))
.with_background(group.overlay);
};
for element in gutter_elements.iter() {
let is_remove = matches!(
element.element_type,
GutterElementType::DiffHunk {
change_type: ChangeType::Remove,
..
}
);
if !is_remove {
flush(&mut group);
continue;
}
let Some(overlay) = element.overlay else {
flush(&mut group);
continue;
};
let current_range = element.line.line_range().clone();
let start_y = element.offset.as_f32();
let end_y = start_y + element.height;
match &mut group {
Some(group) if group.line_range == current_range && group.overlay == overlay => {
group.end_y = end_y;
}
_ => {
flush(&mut group);
group = Some(Group {
start_y,
end_y,
line_range: current_range,
overlay,
});
}
}
}
flush(&mut group);
}
#[allow(clippy::too_many_arguments)]
pub fn new(
editor: InnerEditor<V>,
vertical_expansion_behavior: VerticalExpansionBehavior,
line_number_config: Option<LineNumberConfig>,
diff_status: DiffStatus,
state_handle: EditorWrapperStateHandle,
click_handler: EditorWrapperClickHandler,
should_handle_scroll_wheel: bool,
diff_navigation_state: DiffNavigationState,
focused_diff_line_range: Option<Range<LineCount>>,
add_diff_as_context_button: Option<AddAsContextButton>,
revert_hunk_button: Option<RevertHunkButton>,
comment_button: Option<CommentButton>,
saved_comments: Vec<SavedComment>,
expand_diff_indicator_width_on_hover: bool,
gutter_element_hover_target: GutterHoverTarget,
comment_save_position_id: String,
find_references_save_position_id: String,
) -> Self {
Self {
editor,
vertical_expansion_behavior,
element_size: None,
element_origin: None,
line_number_config,
gutter_elements: None,
diff_status,
state_handle,
diff_navigation_state,
focused_diff_line_range,
click_handler,
should_handle_scroll_wheel,
child_max_z_index: None,
add_hunk_as_context_button: add_diff_as_context_button,
revert_hunk_button,
comment_button,
expand_diff_indicator_width_on_hover,
gutter_element_hover_target,
comment_box: None,
saved_comments,
comment_save_position_id,
find_references_save_position_id,
find_references_anchor: None,
}
}
/// Set the comment box to be displayed at a specific line
pub fn set_comment_box(&mut self, line: EditorLineLocation, app: &AppContext) {
let appearance = Appearance::as_ref(app);
self.comment_box = Some(CommentBox {
line_highlight_element: highlight_element(appearance),
line,
});
}
/// Set the find references anchor line for position caching
pub fn set_find_references_anchor(&mut self, anchor: Option<EditorLineLocation>) {
self.find_references_anchor = anchor;
}
/// True iff the diff hunks are expanded in the underlying editor model.
fn diff_hunks_are_expanded(&self) -> bool {
!matches!(self.diff_navigation_state, DiffNavigationState::Collapsed)
}
/// Returns the saved comment for the given line, if any.
fn find_saved_comment_for_line(&self, line: &EditorLineLocation) -> Option<SavedComment> {
self.saved_comments
.iter()
.find(|comment| comment.location().is_same_line(line))
.cloned()
}
fn should_display_relative_line_number(&self) -> bool {
let Some(line_number_config) = &self.line_number_config else {
return false;
};
if line_number_config.mode != CodeEditorLineNumberMode::Relative
|| line_number_config.active_line_number.is_none()
{
return false;
}
// Relative numbers follow the cursor: only show them when a cursor is
// actually drawn (editor focused and editable).
line_number_config.active_cursor_is_visible
}
/// Returning **no** gutter means the gutter shouldn't be rendered at all.
/// Returning an **empty** gutter means the gutter should be rendered with no contents.
fn gutter_elements(&self, app: &AppContext) -> Option<Vec<GutterElement>> {
let appearance = Appearance::as_ref(app);
let Some(line_number_config) = &self.line_number_config else {
return None;
};
let Some(blocks) = self.editor.blocks() else {
return Some(Vec::new());
};
let mut elements = Vec::new();
let model = self.model().as_ref(app);
let hovered_range = self.state_handle.hovered_diff_hunk.lock();
let line_decorations = model.decorations().line_decoration_ranges();
let last_block_idx = blocks.len().saturating_sub(1);
// Track the index of removal blocks with the same line_count
let mut removed_hunk_line_number: Option<LineCount> = None;
let mut removed_hunk_line_index: usize = 0;
for (block_idx, block) in blocks.iter().enumerate() {
let Some(line_count) = model.start_line_index(&**block) else {
continue;
};
// For lens element, we need to use use the content offset - scroll top instead of the render model's viewport
// offset since we are only rendering a section of the editor.
let offset = match &self.editor {
InnerEditor::Lens(element) => (block.viewport_item().content_offset.as_f32()
- element.starting_renderable_block_offset().unwrap_or(0.))
.into_pixels(),
InnerEditor::FullEditor(_) => block.viewport_item().viewport_offset,
};
let diff_hunk = self.diff_status.diff_hunk(line_count, appearance);
let is_removal = matches!(diff_hunk, Some(DiffHunkDisplay::Remove(_)));
let current_line = if self.should_display_relative_line_number() {
line_number_config.display_line_number(line_count)
} else {
line_number_config.absolute_line_number(line_count)
};
// If the block is temporary, don't render line number.
// Currently, all temporary blocks are removal hunks, either from a deleted section,
// or the old lines from a replacement hunk.
if block.is_temporary() {
let diff_range = self.diff_status.removed_diff_range(line_count);
let range_already_clicked = diff_range
.as_ref()
.is_some_and(|range| self.state_handle.is_range_clicked(range));
// If we are expanding diff hunks and the current block is a removal hunk, render
// the gutter element with the line decoration.
if self.diff_hunks_are_expanded()
&& (is_removal
|| matches!(diff_hunk, Some(DiffHunkDisplay::Replacement { .. })))
{
// Track the index for this removal line
if removed_hunk_line_number == Some(line_count) {
removed_hunk_line_index += 1;
} else {
removed_hunk_line_index = 0;
removed_hunk_line_number = Some(line_count);
}
let height = block.viewport_item().content_size.y();
// Get the first line height for the plus icon (like we do for regular blocks)
let first_line_height = model.first_line_height(&**block).unwrap_or(height);
let line = EditorLineLocation::Removed {
line_number: line_count,
line_range: diff_range.unwrap_or(line_count..line_count),
index: removed_hunk_line_index,
};
// Show comment button only on the specific hovered line with matching index
let is_diff_line = self.diff_hunks_are_expanded() && diff_hunk.is_some();
let range_hovered = hovered_range
.as_ref()
.map(|hovered_line| hovered_line.line_range() == line.line_range())
.unwrap_or(false);
let is_this_line_hovered = hovered_range
.as_ref()
.is_some_and(|hovered_line| hovered_line.is_same_line(&line));
let attached_comment = self.find_saved_comment_for_line(&line);
let is_comment_on_current_line = attached_comment.is_some();
let is_comment_box_open = self.comment_box.is_some();
let is_comment_box_open_on_current_line = self
.comment_box
.as_ref()
.is_some_and(|comment_box| comment_box.line.is_same_line(&line));
let is_comment_box_open_on_different_line =
is_comment_box_open && !is_comment_box_open_on_current_line;
// We want to show the gutter buttons if either:
// 1) This line is part of a diff hunk that is being hovered and the comment box
// isn't open on another line.
// 2) We're currently on a line where the comment box is open.
let show_gutter_buttons = (is_diff_line
&& is_this_line_hovered
&& !range_already_clicked
&& !is_comment_box_open_on_different_line)
|| is_comment_box_open_on_current_line;
// Compute comment visibility independently of diff-hunk buttons
let should_show_comment_button = (is_this_line_hovered
&& !is_comment_box_open_on_different_line)
|| is_comment_box_open_on_current_line;
let element = self.render_gutter_element(
None,
line_number_config,
show_gutter_buttons,
should_show_comment_button,
is_comment_on_current_line,
attached_comment,
first_line_height,
height,
&line,
block.overlay_decoration(),
true, // is diff line
appearance,
);
elements.push(GutterElement {
element,
height,
offset,
hovered: range_hovered,
line,
element_type: GutterElementType::DiffHunk {
hunk: diff_hunk,
change_type: ChangeType::Remove,
},
overlay: block.overlay_decoration(),
});
}
continue;
}
if block.is_hidden_section() {
let line_range = self
.model()
.as_ref(app)
.line_range(&**block)
.unwrap_or_default();
let range_hovered = hovered_range
.as_ref()
.map(|line| line.line_range())
.is_some_and(|hovered_line_range| *hovered_line_range == line_range);
let range_length = line_range.end - line_range.start;
let height = block.viewport_item().content_size.y();
let block_location = if block_idx == 0 {
BlockLocation::Start
} else if block_idx >= last_block_idx {
BlockLocation::End
} else {
BlockLocation::Middle
};
let expand_button_types =
gutter_expansion_button_types(&block_location, range_length.as_usize());
let mut new_gutter_elements = if expand_button_types.len() == 1 {
vec![self.construct_expand_hidden_section_gutter_element(
height,
expand_button_types[0],
range_hovered,
appearance,
line_number_config,
line_range,
offset,
)]
} else {
let half_len = range_length.as_usize() / 2;
let midpoint = line_range.start + half_len;
let (first_half_range, second_half_range) =
(line_range.start..midpoint, midpoint..line_range.end);
let first_range_hovered = hovered_range
.as_ref()
.map(|line| line.line_range().contains(&line_count))
.unwrap_or(false);
let second_range_hovered = hovered_range
.as_ref()
.map(|line| line.line_range().contains(&midpoint))
.unwrap_or(false);
// The editor will render a double-height line,
// so the buttons should split the available vertical space.
let height = height / 2.0;
vec![
self.construct_expand_hidden_section_gutter_element(
height,
expand_button_types[0],
first_range_hovered,
appearance,
line_number_config,
first_half_range,
offset,
),
self.construct_expand_hidden_section_gutter_element(
height,
expand_button_types[1],
second_range_hovered,
appearance,
line_number_config,
second_half_range,
offset + Pixels::new(height),
),
]
};
elements.append(&mut new_gutter_elements);
continue;
}
let diff_range = self.diff_status.added_diff_range(line_count);
let range_already_clicked = diff_range
.as_ref()
.is_some_and(|range| self.state_handle.is_range_clicked(range));
// If the corresponding line in the editor element has a line decoration, we should apply the decoration
// in the wrapper as well. This does assume the line could only have a single decoration. I think it's fine
// given this is a wrapper specific to our code pane implementation.
let overlay = line_decorations
.iter()
.filter(|decoration| decoration.start <= line_count && decoration.end > line_count)
.map(|decoration| decoration.overlay)
.next();
let Some(height) = model.first_line_height(&**block) else {
continue;
};
// Check if this line is part of any diff hunk when diff hunks are expanded and hovered
let is_diff_line = self.diff_hunks_are_expanded() && diff_hunk.is_some();
let line = EditorLineLocation::Current {
line_number: line_count,
line_range: diff_range.unwrap_or(line_count..line_count + 1),
};
let range_hovered = hovered_range
.as_ref()
.is_some_and(|hovered_line| hovered_line.line_range() == line.line_range());
// Show comment button only on the specific hovered line
let is_this_line_hovered = hovered_range
.as_ref()
.is_some_and(|hovered_line| hovered_line.is_same_line(&line));
let attached_comment = self.find_saved_comment_for_line(&line);
let is_comment_box_open = self.comment_box.is_some();
let is_comment_box_open_on_current_line = self
.comment_box
.as_ref()
.is_some_and(|comment_box| comment_box.line.is_same_line(&line));
let is_comment_box_open_on_different_line =
is_comment_box_open && !is_comment_box_open_on_current_line;
// We want to show the gutter buttons if either:
// 1) This line is part of a diff hunk that is being hovered and the comment box
// isn't open on another line.
// 2) We're currently on a line where the comment box is open.
let should_show_diff_hunk_button = (is_diff_line
&& is_this_line_hovered
&& !is_removal
&& !range_already_clicked
&& !is_comment_box_open_on_different_line)
|| is_comment_box_open_on_current_line;
let is_comment_on_current_line = attached_comment.is_some();
// The gutter element should take the same height as block's first line.
// Compute comment visibility for any hovered/current line, not just diff lines
// For non-diff lines, check the feature flag before showing comment button
let should_show_comment_button = {
if !is_diff_line && !FeatureFlag::ContextLineReviewComments.is_enabled() {
// Only show comment button if there's already a comment on this line
is_comment_box_open_on_current_line
} else {
// Show comment button normally for diff lines or when feature flag is enabled
(is_this_line_hovered && !is_comment_box_open_on_different_line)
|| is_comment_box_open_on_current_line
}
};
let element = self.render_gutter_element(
Some(current_line),
line_number_config,
should_show_diff_hunk_button,
should_show_comment_button,
is_comment_on_current_line,
attached_comment,
height,
height,
&line,
overlay,
is_diff_line,
appearance,
);
let diff_hunk = if is_removal && self.diff_hunks_are_expanded() {
None
} else {
diff_hunk
};
elements.push(GutterElement {
element,
height,
offset,
hovered: range_hovered,
// We can skip rendering this removal gutter element if its hunk is expanded since
// the gutter is rendered on the temporary block.
line,
element_type: GutterElementType::DiffHunk {
hunk: diff_hunk,
change_type: ChangeType::Add,
},
overlay: None,
});
}
Some(elements)
}
#[allow(clippy::too_many_arguments)]
fn construct_expand_hidden_section_gutter_element(
&self,
height: f32,
expansion_type: ExpansionType,
range_hovered: bool,
appearance: &Appearance,
line_number_config: &LineNumberConfig,
line_range: Range<LineCount>,
offset: Pixels,
) -> GutterElement {
// Use a slightly stronger overlay when hovered for better visual feedback
let theme = appearance.theme();
let gutter_background_color = if range_hovered {
internal_colors::fg_overlay_2(theme)
} else {
internal_colors::fg_overlay_1(theme)
};
let icon = ConstrainedBox::new(
galaxyui::elements::Icon::new(
expansion_type.icon().into(),
line_number_config.text_color,
)
.finish(),
)
.with_width(16.)
.with_height(16.)
.finish();
let element = Container::new(
ConstrainedBox::new(Align::new(icon).finish())
.with_height(height)
.with_width(GUTTER_WIDTH)
.finish(),
)
.with_background_color(gutter_background_color.into())
.finish();
GutterElement {
element,
offset,
height,
hovered: range_hovered,
line: EditorLineLocation::Collapsed { line_range },
element_type: GutterElementType::HiddenSection { expansion_type },
overlay: None,
}
}
fn render_gutter_button(
&self,
mouse_state: MouseStateHandle,
gutter_element_height: f32,
on_click_action: Option<CodeEditorViewAction>,
appearance: &Appearance,
gutter_button: &dyn GutterButton,
) -> Box<dyn Element> {
let vertical_padding = if FeatureFlag::InlineCodeReview.is_enabled() {
2.
} else {
4.
};
let button_size = gutter_element_height;
let icon_size = button_size - (vertical_padding * 2.);
let enabled = gutter_button.is_enabled();
let mut button = Hoverable::new(mouse_state, |state| {
let button_background = gutter_button.background_color(state, appearance);
let icon_color = gutter_button.icon_color(state, appearance);
let border = SecondaryTheme.border(appearance);
let container = Container::new(
ConstrainedBox::new(
galaxyui::elements::Icon::new(gutter_button.icon().into(), icon_color).finish(),
)
.with_width(icon_size)
.with_height(icon_size)
.finish(),
)
.with_uniform_padding(2.)
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
.with_background(button_background);
let container = if let Some(border) = border {
container.with_border(Border::all(1.0).with_border_fill(border))
} else {
container
};
let mut stack = Stack::new().with_child(container.finish());
if state.is_hovered() {
if let Some(text) = gutter_button.tooltip_text() {
let tooltip = appearance
.ui_builder()
.tool_tip(text.into())
.build()
.finish();
stack.add_positioned_overlay_child(
tooltip,
OffsetPositioning::offset_from_parent(
vec2f(0., 8.),
ParentOffsetBounds::Unbounded,
ParentAnchor::BottomLeft,
ChildAnchor::TopLeft,
),
);
}
}
stack.finish()
});
if enabled {
button = button.with_cursor(galaxyui::platform::Cursor::PointingHand);
if let Some(on_click_action) = on_click_action {
let action = on_click_action.clone();
button = button.on_click(move |event, _app, _position| {
event.dispatch_typed_action(action.clone());
});
}
}
button.finish()
}
/// Renders the plus button for adding a diff as Agent context.
fn render_plus_button(
&self,
add_as_context_button: &AddAsContextButton,
gutter_element_height: f32,
diff_line_range: &Range<LineCount>,
appearance: &Appearance,
) -> Box<dyn Element> {
let on_click_action = Some(CodeEditorViewAction::AddDiffHunkContext {
line_range: diff_line_range.to_owned(),
});
self.render_gutter_button(
self.state_handle.add_as_context_mouse_state.clone(),
gutter_element_height,
on_click_action,
appearance,
add_as_context_button,
)
}
/// Renders the revert button for reverting a specific diff hunk.
fn render_revert_button(
&self,
revert_hunk_button: &RevertHunkButton,
gutter_element_height: f32,
diff_line_range: &Range<LineCount>,
appearance: &Appearance,
) -> Box<dyn Element> {
let on_click_action = Some(CodeEditorViewAction::RevertDiffHunk {
line_range: diff_line_range.to_owned(),
});
self.render_gutter_button(
self.state_handle.revert_mouse_state.clone(),
gutter_element_height,
on_click_action,
appearance,
revert_hunk_button,
)
}
/// Renders the comment button for adding comments to diff hunks.
fn render_comment_button(
&self,
gutter_element_height: f32,
line: &EditorLineLocation,
attached_comment: Option<SavedComment>,
appearance: &Appearance,
comment_button: &CommentButton,
) -> Box<dyn Element> {
let (on_click_action, comment_button, mouse_state) = if self
.comment_box
.as_ref()
.is_some_and(|comment_box| comment_box.line.is_same_line(line))
{
let comment_button = if attached_comment.is_some() {
CommentButton::EditorOpenedToUpdateComment
} else {
CommentButton::EditorOpenedToCreateNewComment
};
// If the comment box is already open for this line, don't do anything on click
(
None,
comment_button,
self.state_handle.comment_mouse_state.clone(),
)
} else {
match attached_comment {
Some(saved_comment) => (
Some(CodeEditorViewAction::RequestOpenSavedComment {
uuid: saved_comment.uuid(),
}),
CommentButton::AddedComment,
saved_comment.mouse_state().clone(),
),
None => (
Some(CodeEditorViewAction::NewCommentOnLine { line: line.clone() }),
*comment_button,
self.state_handle.comment_mouse_state.clone(),
),
}
};
self.render_gutter_button(
mouse_state,
gutter_element_height,
on_click_action,
appearance,
&comment_button,
)
}
#[allow(clippy::too_many_arguments)]
fn render_gutter_element(
&self,
current_line: Option<usize>,
line_number_config: &LineNumberConfig,
should_show_diff_hunk_icons: bool,
should_show_comment_button: bool,
is_active_comment_on_current_line: bool,
attached_comment: Option<SavedComment>,
line_height: f32,
gutter_element_height: f32,
line: &EditorLineLocation,
overlay: Option<Fill>,
highlight_text: bool,
appearance: &Appearance,
) -> Box<dyn Element> {
let base_content = match current_line {
Some(line) => {
// Highlight the line number if it's a diff line or has an overlay
let text_color = if highlight_text || overlay.is_some() {
line_number_config.highlight_text_color
} else {
line_number_config.text_color
};
Align::new(
Text::new_inline(
line.to_string(),
line_number_config.font_family,
line_number_config.font_size,
)
.with_selectable(true)
.with_color(text_color)
.finish(),
)
.finish()
}
None => {
// If no current line, render empty element
Empty::new().finish()
}
};
let constrained_base = ConstrainedBox::new(base_content)
.with_height(gutter_element_height)
.with_width(GUTTER_WIDTH)
.finish();
let show_add_as_context_button = self.add_hunk_as_context_button.is_some();
let show_revert_diff_hunk =
FeatureFlag::RevertDiffHunk.is_enabled() && self.revert_hunk_button.is_some();
// Show comment button independently of diff hunk state when requested
let show_comment_button = FeatureFlag::InlineCodeReview.is_enabled()
&& self.comment_button.is_some()
&& (should_show_comment_button || is_active_comment_on_current_line);
if should_show_diff_hunk_icons || is_active_comment_on_current_line || show_comment_button {
let mut buttons = Flex::row().with_main_axis_size(MainAxisSize::Min);
if let Some(comment_button) =
self.comment_button.as_ref().filter(|_| show_comment_button)
{
buttons.add_child(self.render_comment_button(
line_height,
line,
attached_comment,
appearance,
comment_button,
));
}
if should_show_diff_hunk_icons {
if let Some(revert_hunk_button) = self
.revert_hunk_button
.as_ref()
.filter(|_| show_revert_diff_hunk)
{
buttons.add_child(self.render_revert_button(
revert_hunk_button,
line_height,
line.line_range(),
appearance,
));
}
if let Some(add_as_context_button) = self
.add_hunk_as_context_button
.as_ref()
.filter(|_| show_add_as_context_button)
{
buttons.add_child(self.render_plus_button(
add_as_context_button,
line_height,
line.line_range(),
appearance,
));
}
}
let offset = if self.expand_diff_indicator_width_on_hover {
vec2f(VERTICAL_DIFF_HUNK_INDICATOR_HOVERED_WIDTH, 0.)
} else {
vec2f(VERTICAL_DIFF_HUNK_INDICATOR_WIDTH, 0.)
};
// Create a stack with the constrained base and overlay the buttons
let mut main_stack = Stack::new().with_child(constrained_base);
main_stack.add_positioned_child(
buttons.finish(),
OffsetPositioning::offset_from_parent(
offset,
ParentOffsetBounds::Unbounded,
ParentAnchor::TopLeft,
ChildAnchor::TopLeft,
),
);
main_stack.finish()
} else {
constrained_base
}
}
/// Returns whether a diff hunk range contains the given mouse position on the screen.
fn gutter_element_range_containing_position(
&self,
position: Vector2F,
check_y_axis: bool,
) -> Option<GutterRange> {
let wrapper_origin = self.element_origin?.xy();
let wrapper_size = self.size()?;
if let Some(gutter_elements) = &self.gutter_elements {
for gutter_element in gutter_elements {
let gutter_range = gutter_element.contains_position(
position,
wrapper_origin,
wrapper_size,
check_y_axis,
);
if let Some(gutter_range) = gutter_range {
return Some(gutter_range);
}
}
}
None
}
fn model(&self) -> &ModelHandle<RenderState> {
self.editor.model()
}
fn size_buffer(&self) -> Vector2F {
let is_gutter_present = self.line_number_config.is_some();
if is_gutter_present {
GUTTER_WIDTH.along(Axis::Horizontal)
} else {
Vector2F::zero()
}
}
}
impl<V: EditorView> Element for EditorWrapper<V> {
fn layout(
&mut self,
constraint: SizeConstraint,
ctx: &mut LayoutContext,
app: &AppContext,
) -> Vector2F {
let size_buffer = self.size_buffer();
let content_constraint = SizeConstraint::new(
(constraint.min - size_buffer).max(Vector2F::zero()),
(constraint.max - size_buffer).max(Vector2F::zero()),
);
// Layout the editor element first so we can read the laid out visible blocks.
let editor_size = self.editor.layout(content_constraint, ctx, app);
let size = match self.vertical_expansion_behavior {
VerticalExpansionBehavior::GrowToMaxHeight
| VerticalExpansionBehavior::InfiniteHeight => {
Vector2F::new(constraint.max.x(), constraint.max.y().min(editor_size.y()))
}
VerticalExpansionBehavior::FillMaxHeight => constraint.max,
};
let mut gutter_elements = self.gutter_elements(app);
if let Some(gutter_elements) = &mut gutter_elements {
for gutter_element in gutter_elements {
let gutter_element_size = gutter_element.element.layout(constraint, ctx, app);
if FeatureFlag::InlineCodeReview.is_enabled() {
if let Some(comment_box) = &mut self.comment_box {
let highlight_line = &comment_box.line;
if gutter_element.line == *highlight_line {
let highlight_width = size.x();
let highlight_height = gutter_element_size.y();
comment_box.line_highlight_element.layout(
SizeConstraint {
min: vec2f(0.0, 0.0),
max: vec2f(highlight_width, highlight_height),
},
ctx,
app,
);
}
}
}
}
}
self.gutter_elements = gutter_elements;
self.element_size = Some(size);
size
}
fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) {
self.editor.after_layout(ctx, app);
}
fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
// Save the element origin for hit testing.
let element_origin = Point::from_vec2f(origin, ctx.scene.z_index());
self.element_origin = Some(element_origin);
let size_buffer = self.size_buffer();
let wrapper_size = self.size().unwrap_or_default();
// Pre-pass: Draw full-width overlay rects for diff highlighting.
// Drawing before the inner editor and gutter elements so they appear behind text.
// Clip to the wrapper bounds so overlays don't bleed outside the element
// (important for Lens mode where LineDecoration ranges may exceed the visible range).
let overlay_clip = RectF::new(origin, wrapper_size);
ctx.scene
.start_layer(ClipBounds::BoundedByActiveLayerAnd(overlay_clip));
// Added/replaced lines: one rect per LineDecoration range.
{
let model = self.model().as_ref(app);
let content = model.content();
let y_adjustment = match &self.editor {
InnerEditor::Lens(element) => element
.starting_renderable_block_offset()
.unwrap_or(0.)
.into_pixels(),
InnerEditor::FullEditor(_) => model.viewport().scroll_top(),
};
for decoration in model.decorations().line_decoration_ranges() {
let start_y = content.y_offset_at_line(decoration.start);
let end_y = content.y_offset_at_line(decoration.end);
ctx.scene
.draw_rect_without_hit_recording(RectF::new(
origin + vec2f(0., (start_y - y_adjustment).as_f32()),
vec2f(wrapper_size.x(), (end_y - start_y).as_f32()),
))
.with_background(decoration.overlay);
}
}
self.paint_removed_line_overlays(origin, wrapper_size, ctx);
ctx.scene.stop_layer();
self.editor.paint(origin + size_buffer, ctx, app);
let diff_hunks_are_expanded = self.diff_hunks_are_expanded();
let gutter_width = self.size_buffer().x();
let gutter_bounds = RectF::new(origin, vec2f(gutter_width, wrapper_size.y()));
// Track the offset and height of the gutter element under which we should render the
// inline comment box.
let mut inline_comment_gutter_element: Option<(f32, f32)> = None;
// Track the offset and height of the gutter element for find references anchor.
let mut find_references_gutter_element: Option<(f32, f32)> = None;
if let Some(gutter_elements) = &mut self.gutter_elements {
// Start clipping layer for gutter elements to prevent overflow
// at the bottom of the editor.
// Use BoundedByActiveLayerAnd to intersect with parent scrollable's clipping.
ctx.scene
.start_layer(ClipBounds::BoundedByActiveLayerAnd(gutter_bounds));
// Combined pass: compute grouped diff indicator sliver rects and
// track positions for inline comment / find references.
// Adjacent indicators in the same diff range are grouped into one tall rect.
struct SliverGroup<'a> {
start_y: f32,
end_y: f32,
range: &'a Range<LineCount>,
color: ColorU,
width: f32,
}
let mut sliver_rects: Vec<(RectF, ColorU)> = Vec::new();
let mut group: Option<SliverGroup<'_>> = None;
for element in gutter_elements.iter() {
let gutter_y = element.offset.as_f32();
// For rendering, only expand the gutter element size if it is
// hovered or it is the active diff hunk.
let is_hovered = self.expand_diff_indicator_width_on_hover
&& (element.hovered
|| self
.focused_diff_line_range
.as_ref()
.is_some_and(|r| r == element.line.line_range()));
if let Some(size) = element.diff_hunk_size(is_hovered) {
if let Some(color) = element.diff_indicator_color(diff_hunks_are_expanded) {
let current_range = element.line.line_range();
let same_group = group.as_ref().is_some_and(|g| {
g.range == current_range
&& g.color == color
&& (g.width - size.x()).abs() < f32::EPSILON
});
if same_group {
group.as_mut().unwrap().end_y = gutter_y + element.height;
} else {
// Flush previous group.
if let Some(g) = group.take() {
sliver_rects.push((
RectF::new(
origin + vec2f(0., g.start_y),
vec2f(g.width, g.end_y - g.start_y),
),
g.color,
));
}
// Start new group.
group = Some(SliverGroup {
start_y: gutter_y,
end_y: gutter_y + element.height,
range: current_range,
color,
width: size.x(),
});
}
}
}
// Track positions for inline comment and find references.
if !matches!(
element.element_type,
GutterElementType::HiddenSection { .. }
) {
// If this is the gutter element for the inline comment box,
// save its position for later rendering.
if self
.comment_box
.as_ref()
.is_some_and(|cb| element.line == cb.line)
{
inline_comment_gutter_element = Some((gutter_y, element.height));
}
// If this is the gutter element for find references anchor,
// save its position for caching.
if self
.find_references_anchor
.as_ref()
.is_some_and(|anchor| element.line.is_same_line(anchor))
{
find_references_gutter_element = Some((gutter_y, element.height));
}
}
}
// Flush last group.
if let Some(g) = group.take() {
sliver_rects.push((
RectF::new(
origin + vec2f(0., g.start_y),
vec2f(g.width, g.end_y - g.start_y),
),
g.color,
));
}
// Draw all buffered sliver rects (behind gutter elements).
for (rect, color) in &sliver_rects {
ctx.scene
.draw_rect_with_hit_recording(*rect)
.with_background(*color);
}
// Paint gutter elements.
for gutter_element in gutter_elements.iter_mut() {
let gutter_origin = origin + vec2f(0., gutter_element.offset.as_f32());
gutter_element.element.paint(gutter_origin, ctx, app);
}
ctx.scene.stop_layer();
if FeatureFlag::InlineCodeReview.is_enabled() {
if let Some(comment_box) = &mut self.comment_box {
if let Some((offset, height)) = inline_comment_gutter_element {
let gutter_origin = origin + vec2f(0., offset);
// Highlight the selected line.
comment_box
.line_highlight_element
.paint(gutter_origin, ctx, app);
// Cache a single pixel where the comment editor would be located
// if were to render it within this element.
let rect = RectF::new(gutter_origin, vec2f(1., height));
ctx.position_cache.cache_position_for_one_frame(
self.comment_save_position_id.clone(),
rect,
);
}
}
}
}
// Cache find references anchor position if we have one.
if self.find_references_anchor.is_some() {
if let Some((offset, height)) = find_references_gutter_element {
let gutter_origin = origin + vec2f(0., offset);
// Cache the gutter position for the find references anchor.
// This will be used to determine if the card should be shown.
let rect = RectF::new(gutter_origin, vec2f(1., height));
ctx.position_cache.cache_position_for_one_frame(
self.find_references_save_position_id.clone(),
rect,
);
}
}
self.child_max_z_index = Some(ctx.scene.max_active_z_index());
}
fn size(&self) -> Option<Vector2F> {
self.element_size
}
fn origin(&self) -> Option<Point> {
self.element_origin
}
fn dispatch_event(
&mut self,
event: &DispatchedEvent,
ctx: &mut EventContext,
app: &AppContext,
) -> bool {
let Some(z_index) = self
.child_max_z_index
.or_else(|| self.element_origin.map(|origin| origin.z_index()))
else {
return false;
};
// Then, dispatch events to gutter elements that are in the hovered range
// This is important for Hoverable elements like the plus button
let mut gutter_handled = false;
if let Some(gutter_elements) = &mut self.gutter_elements {
let hovered_range = self.state_handle.hovered_diff_hunk.lock().clone();
for gutter_element in gutter_elements {
let should_dispatch = hovered_range
.as_ref()
.is_some_and(|hovered_line| hovered_line.is_same_line(&gutter_element.line));
if should_dispatch && gutter_element.element.dispatch_event(event, ctx, app) {
gutter_handled = true;
}
}
}
// Handle mouse events for hover state and clicks
match event.at_z_index(z_index, ctx) {
Some(Event::MouseMoved { position, .. }) => {
let only_check_y_axis =
matches!(self.gutter_element_hover_target, GutterHoverTarget::Line);
let hovered_line = self
.gutter_element_range_containing_position(*position, only_check_y_axis)
.map(|gutter_range| gutter_range.line().clone());
let mut hovered_diff_hunk = self.state_handle.hovered_diff_hunk.lock();
if hovered_diff_hunk.as_ref() != hovered_line.as_ref() {
// When hovering over a new range, clear the previously clicked range
// so the add context button can appear again
if hovered_line.is_some() {
self.state_handle.clear_clicked_range();
}
*hovered_diff_hunk = hovered_line;
ctx.notify();
}
}
Some(Event::LeftMouseDown { position, .. }) => {
if !gutter_handled {
let in_bound = self
.gutter_element_range_containing_position(*position, false)
.is_some();
self.state_handle
.in_click
.store(in_bound, Ordering::Relaxed);
}
}
Some(Event::LeftMouseUp { position, .. }) => {
if !gutter_handled {
let was_clicking = self.state_handle.in_click.swap(false, Ordering::Relaxed);
if was_clicking {
if let Some(gutter_range) =
self.gutter_element_range_containing_position(*position, false)
{
(self.click_handler)(gutter_range, ctx);
}
}
}
}
_ => (),
};
let dispatch_to_editor = self.editor.dispatch_event(event, ctx, app);
// Always dispatch to both the gutter elements and the editor.
dispatch_to_editor || gutter_handled
}
}
impl<V: EditorView> NewScrollableElement for EditorWrapper<V> {
fn scroll_data(&self, axis: Axis, app: &AppContext) -> Option<ScrollData> {
// TODO: Support scrolling in editor lens.
match &self.editor {
InnerEditor::FullEditor(element) => element.scroll_data(axis, app),
InnerEditor::Lens(_) => None,
}
}
fn scroll(&mut self, delta: Pixels, axis: Axis, ctx: &mut EventContext) {
if let InnerEditor::FullEditor(element) = &mut self.editor {
element.scroll(delta, axis, ctx)
}
}
fn axis_should_handle_scroll_wheel(&self, _axis: Axis) -> bool {
self.should_handle_scroll_wheel
}
fn axis(&self) -> ScrollableAxis {
ScrollableAxis::Both
}
}
#[cfg(test)]
#[path = "element_tests.rs"]
mod tests;