Compare commits
14 Commits
fix-code-a
...
xcode-styl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06f4f46504 | ||
|
|
fb877ddf4c | ||
|
|
5135326294 | ||
|
|
4acdb447cf | ||
|
|
1e91d68e08 | ||
|
|
4276901e28 | ||
|
|
44152c412f | ||
|
|
c19ff51465 | ||
|
|
f41747b422 | ||
|
|
f8d20986a1 | ||
|
|
82fa6d7e53 | ||
|
|
d5392cf53f | ||
|
|
a07a090b5a | ||
|
|
046dbba964 |
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -4314,11 +4314,14 @@ dependencies = [
|
||||
"client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"db",
|
||||
"debugger_tools",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"feature_flags",
|
||||
"file_icons",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
|
||||
@@ -303,6 +303,7 @@ pub enum ComponentScope {
|
||||
Collaboration,
|
||||
#[strum(serialize = "Data Display")]
|
||||
DataDisplay,
|
||||
Debugger,
|
||||
Editor,
|
||||
#[strum(serialize = "Images & Icons")]
|
||||
Images,
|
||||
|
||||
@@ -32,10 +32,14 @@ bitflags.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
dap.workspace = true
|
||||
dap_adapters = { workspace = true, optional = true }
|
||||
db.workspace = true
|
||||
debugger_tools = { workspace = true, optional = true }
|
||||
editor.workspace = true
|
||||
env_logger = { workspace = true, optional = true }
|
||||
feature_flags.workspace = true
|
||||
file_icons.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
@@ -64,11 +68,10 @@ theme.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
tree-sitter-json.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
debugger_tools = { workspace = true, optional = true }
|
||||
unindent = { workspace = true, optional = true }
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -21,6 +21,7 @@ mod editor_settings;
|
||||
mod editor_settings_controls;
|
||||
mod element;
|
||||
mod git;
|
||||
mod gutter;
|
||||
mod highlight_matching_bracket;
|
||||
mod hover_links;
|
||||
pub mod hover_popover;
|
||||
@@ -201,8 +202,8 @@ use theme::{
|
||||
observe_buffer_font_size_adjustment,
|
||||
};
|
||||
use ui::{
|
||||
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
|
||||
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*,
|
||||
ButtonSize, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName, IconSize,
|
||||
Indicator, Key, Tooltip, h_flex, prelude::*,
|
||||
};
|
||||
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
|
||||
use workspace::{
|
||||
@@ -7977,121 +7978,6 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_breakpoint(
|
||||
&self,
|
||||
position: Anchor,
|
||||
row: DisplayRow,
|
||||
breakpoint: &Breakpoint,
|
||||
state: Option<BreakpointSessionState>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> IconButton {
|
||||
let is_rejected = state.is_some_and(|s| !s.verified);
|
||||
// Is it a breakpoint that shows up when hovering over gutter?
|
||||
let (is_phantom, collides_with_existing) = self.gutter_breakpoint_indicator.0.map_or(
|
||||
(false, false),
|
||||
|PhantomBreakpointIndicator {
|
||||
is_active,
|
||||
display_row,
|
||||
collides_with_existing_breakpoint,
|
||||
}| {
|
||||
(
|
||||
is_active && display_row == row,
|
||||
collides_with_existing_breakpoint,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let (color, icon) = {
|
||||
let icon = match (&breakpoint.message.is_some(), breakpoint.is_disabled()) {
|
||||
(false, false) => ui::IconName::DebugBreakpoint,
|
||||
(true, false) => ui::IconName::DebugLogBreakpoint,
|
||||
(false, true) => ui::IconName::DebugDisabledBreakpoint,
|
||||
(true, true) => ui::IconName::DebugDisabledLogBreakpoint,
|
||||
};
|
||||
|
||||
let color = if is_phantom {
|
||||
Color::Hint
|
||||
} else if is_rejected {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Debugger
|
||||
};
|
||||
|
||||
(color, icon)
|
||||
};
|
||||
|
||||
let breakpoint = Arc::from(breakpoint.clone());
|
||||
|
||||
let alt_as_text = gpui::Keystroke {
|
||||
modifiers: Modifiers::secondary_key(),
|
||||
..Default::default()
|
||||
};
|
||||
let primary_action_text = if breakpoint.is_disabled() {
|
||||
"Enable breakpoint"
|
||||
} else if is_phantom && !collides_with_existing {
|
||||
"Set breakpoint"
|
||||
} else {
|
||||
"Unset breakpoint"
|
||||
};
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let meta = if is_rejected {
|
||||
SharedString::from("No executable code is associated with this line.")
|
||||
} else if collides_with_existing && !breakpoint.is_disabled() {
|
||||
SharedString::from(format!(
|
||||
"{alt_as_text}-click to disable,\nright-click for more options."
|
||||
))
|
||||
} else {
|
||||
SharedString::from("Right-click for more options.")
|
||||
};
|
||||
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ui::ButtonSize::None)
|
||||
.when(is_rejected, |this| {
|
||||
this.indicator(Indicator::icon(Icon::new(IconName::Warning)).color(Color::Warning))
|
||||
})
|
||||
.icon_color(color)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(cx.listener({
|
||||
let breakpoint = breakpoint.clone();
|
||||
|
||||
move |editor, event: &ClickEvent, window, cx| {
|
||||
let edit_action = if event.modifiers().platform || breakpoint.is_disabled() {
|
||||
BreakpointEditAction::InvertState
|
||||
} else {
|
||||
BreakpointEditAction::Toggle
|
||||
};
|
||||
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
position,
|
||||
breakpoint.as_ref().clone(),
|
||||
edit_action,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
Some(position),
|
||||
event.down.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta_in(
|
||||
primary_action_text,
|
||||
Some(&ToggleBreakpoint),
|
||||
meta.clone(),
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn build_tasks_context(
|
||||
project: &Entity<Project>,
|
||||
buffer: &Entity<Buffer>,
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::{
|
||||
ScrollbarDiagnostics, ShowMinimap, ShowScrollbar,
|
||||
},
|
||||
git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
|
||||
gutter::breakpoint_indicator::breakpoint_indicator_path,
|
||||
hover_popover::{
|
||||
self, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
|
||||
POPOVER_RIGHT_OFFSET, hover_at,
|
||||
@@ -30,6 +31,7 @@ use crate::{
|
||||
mouse_context_menu::{self, MenuPosition},
|
||||
scroll::{ActiveScrollbarState, ScrollbarThumbState, scroll_amount::ScrollAmount},
|
||||
};
|
||||
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use file_icons::FileIcons;
|
||||
@@ -42,12 +44,12 @@ use gpui::{
|
||||
Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
|
||||
Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges,
|
||||
Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
|
||||
HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length,
|
||||
HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString,
|
||||
Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, WeakEntity,
|
||||
Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px,
|
||||
quad, relative, size, solid_background, transparent_black,
|
||||
Window, anchored, canvas, deferred, div, fill, linear_color_stop, linear_gradient, outline,
|
||||
point, px, quad, relative, size, solid_background, transparent_black,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::{
|
||||
@@ -61,7 +63,7 @@ use multi_buffer::{
|
||||
|
||||
use project::{
|
||||
ProjectPath,
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointEditAction, BreakpointSessionState},
|
||||
project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
|
||||
};
|
||||
use settings::Settings;
|
||||
@@ -2757,7 +2759,16 @@ impl EditorElement {
|
||||
return None;
|
||||
}
|
||||
|
||||
let button = editor.render_breakpoint(text_anchor, display_row, &bp, state, cx);
|
||||
let button = self.render_breakpoint(
|
||||
editor,
|
||||
snapshot,
|
||||
text_anchor,
|
||||
display_row,
|
||||
row,
|
||||
&bp,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let button = prepaint_gutter_button(
|
||||
button,
|
||||
@@ -2776,6 +2787,184 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_breakpoint(
|
||||
&self,
|
||||
editor: &Editor,
|
||||
snapshot: &EditorSnapshot,
|
||||
position: Anchor,
|
||||
row: DisplayRow,
|
||||
multibuffer_row: MultiBufferRow,
|
||||
breakpoint: &Breakpoint,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let element_id =
|
||||
ElementId::Name(format!("breakpoint_indicator_{}", multibuffer_row.0).into());
|
||||
|
||||
// === Extract text style and calculate dimensions ===
|
||||
let text_style = self.style.text.clone();
|
||||
let font_size = text_style.font_size;
|
||||
let font_size_px = font_size.to_pixels(window.rem_size());
|
||||
let rem_size = window.rem_size();
|
||||
|
||||
// Calculate font scale relative to a baseline font size
|
||||
const BASELINE_FONT_SIZE: f32 = 14.0; // Default editor font size
|
||||
let font_scale = font_size_px / px(BASELINE_FONT_SIZE);
|
||||
|
||||
let line_height: Pixels = text_style.line_height.to_pixels(font_size, rem_size);
|
||||
|
||||
// Debug font metrics
|
||||
dbg!(font_size);
|
||||
dbg!(font_size_px);
|
||||
dbg!(rem_size);
|
||||
dbg!(BASELINE_FONT_SIZE);
|
||||
dbg!(font_scale);
|
||||
dbg!(line_height);
|
||||
|
||||
// Helper to scale pixel values based on font size
|
||||
let scale_px = |value: f32| px(value) * font_scale;
|
||||
|
||||
const HORIZONTAL_OFFSET: f32 = 40.0;
|
||||
const VERTICAL_OFFSET: f32 = 4.0;
|
||||
|
||||
let horizontal_offset = scale_px(HORIZONTAL_OFFSET);
|
||||
let vertical_offset = px(VERTICAL_OFFSET);
|
||||
let indicator_height = line_height - vertical_offset;
|
||||
|
||||
let line_number_width = self.max_line_number_width(snapshot, window, cx);
|
||||
let indicator_width = dbg!(line_number_width) + scale_px(HORIZONTAL_OFFSET);
|
||||
|
||||
// Debug indicator dimensions
|
||||
dbg!(horizontal_offset);
|
||||
dbg!(vertical_offset);
|
||||
dbg!(indicator_height);
|
||||
dbg!(indicator_width);
|
||||
|
||||
let is_disabled = breakpoint.is_disabled();
|
||||
let breakpoint_arc = Arc::from(breakpoint.clone());
|
||||
|
||||
let (is_hovered, collides_with_existing) = editor.gutter_breakpoint_indicator.0.map_or(
|
||||
(false, false),
|
||||
|PhantomBreakpointIndicator {
|
||||
is_active,
|
||||
display_row,
|
||||
collides_with_existing_breakpoint,
|
||||
}| {
|
||||
(
|
||||
is_active && display_row == row,
|
||||
collides_with_existing_breakpoint,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let indicator_color = if is_hovered {
|
||||
cx.theme().colors().ghost_element_hover
|
||||
} else if is_disabled {
|
||||
cx.theme().status().info.alpha(0.64)
|
||||
} else {
|
||||
cx.theme().status().info.alpha(0.48)
|
||||
};
|
||||
|
||||
let primary_action = if is_disabled {
|
||||
"enable"
|
||||
} else if is_hovered && !collides_with_existing {
|
||||
"set"
|
||||
} else {
|
||||
"unset"
|
||||
};
|
||||
|
||||
let mut tooltip_text = format!("Click to {primary_action}");
|
||||
|
||||
if collides_with_existing && !is_disabled {
|
||||
use std::fmt::Write;
|
||||
let modifier_key = gpui::Keystroke {
|
||||
modifiers: Modifiers::secondary_key(),
|
||||
..Default::default()
|
||||
};
|
||||
write!(tooltip_text, ", {modifier_key}-click to disable").ok();
|
||||
}
|
||||
|
||||
div()
|
||||
.id(element_id)
|
||||
.cursor_pointer()
|
||||
.absolute()
|
||||
.left_0()
|
||||
.w(indicator_width)
|
||||
.h(indicator_height)
|
||||
.child(
|
||||
canvas(
|
||||
|_bounds, _cx, _style| {},
|
||||
move |bounds, _cx, window, _style| {
|
||||
// Debug canvas bounds
|
||||
dbg!(&bounds);
|
||||
|
||||
// Adjust bounds to account for horizontal offset
|
||||
let adjusted_bounds = Bounds {
|
||||
origin: point(bounds.origin.x + horizontal_offset, bounds.origin.y),
|
||||
size: size(bounds.size.width, indicator_height),
|
||||
};
|
||||
|
||||
// Debug adjusted bounds
|
||||
dbg!(&adjusted_bounds);
|
||||
dbg!(font_scale);
|
||||
|
||||
// Generate the breakpoint indicator path
|
||||
let path =
|
||||
breakpoint_indicator_path(adjusted_bounds, font_scale, is_disabled);
|
||||
|
||||
// Paint the path with the calculated color
|
||||
window.paint_path(path, indicator_color);
|
||||
},
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
.on_click({
|
||||
let editor_weak = self.editor.downgrade();
|
||||
let breakpoint = breakpoint_arc.clone();
|
||||
move |event, window, cx| {
|
||||
let action = if event.modifiers().platform || breakpoint.is_disabled() {
|
||||
BreakpointEditAction::InvertState
|
||||
} else {
|
||||
BreakpointEditAction::Toggle
|
||||
};
|
||||
|
||||
let Some(editor_strong) = editor_weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
window.focus(&editor_strong.focus_handle(cx));
|
||||
editor_strong.update(cx, |editor, cx| {
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
position,
|
||||
breakpoint.as_ref().clone(),
|
||||
action,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.on_mouse_down(gpui::MouseButton::Right, {
|
||||
let editor_weak = self.editor.downgrade();
|
||||
let anchor_position = position.clone();
|
||||
move |event, window, cx| {
|
||||
let Some(editor_strong) = editor_weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
editor_strong.update(cx, |editor, cx| {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
Some(anchor_position.clone()),
|
||||
event.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
@@ -3034,7 +3223,7 @@ impl EditorElement {
|
||||
scroll_position: gpui::Point<f32>,
|
||||
rows: Range<DisplayRow>,
|
||||
buffer_rows: &[RowInfo],
|
||||
active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
|
||||
_active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
|
||||
newest_selection_head: Option<DisplayPoint>,
|
||||
snapshot: &EditorSnapshot,
|
||||
window: &mut Window,
|
||||
@@ -3090,16 +3279,7 @@ impl EditorElement {
|
||||
return None;
|
||||
}
|
||||
|
||||
let color = active_rows
|
||||
.get(&display_row)
|
||||
.map(|spec| {
|
||||
if spec.breakpoint {
|
||||
cx.theme().colors().debugger_accent
|
||||
} else {
|
||||
cx.theme().colors().editor_active_line_number
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_line_number);
|
||||
let color = cx.theme().colors().editor_active_line_number;
|
||||
let shaped_line =
|
||||
self.shape_line_number(SharedString::from(&line_number), color, window);
|
||||
let scroll_top = scroll_position.y * line_height;
|
||||
@@ -5577,6 +5757,9 @@ impl EditorElement {
|
||||
cx: &mut App,
|
||||
) {
|
||||
window.paint_layer(layout.gutter_hitbox.bounds, |window| {
|
||||
for breakpoint in layout.breakpoints.iter_mut() {
|
||||
breakpoint.paint(window, cx);
|
||||
}
|
||||
window.with_element_namespace("crease_toggles", |window| {
|
||||
for crease_toggle in layout.crease_toggles.iter_mut().flatten() {
|
||||
crease_toggle.paint(window, cx);
|
||||
@@ -5589,13 +5772,24 @@ impl EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
for breakpoint in layout.breakpoints.iter_mut() {
|
||||
breakpoint.paint(window, cx);
|
||||
}
|
||||
|
||||
for test_indicator in layout.test_indicators.iter_mut() {
|
||||
test_indicator.paint(window, cx);
|
||||
}
|
||||
|
||||
let show_git_gutter = layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.show_git_diff_gutter
|
||||
.unwrap_or_else(|| {
|
||||
matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
)
|
||||
});
|
||||
|
||||
if show_git_gutter {
|
||||
Self::paint_gutter_diff_hunks(layout, window, cx)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5619,20 +5813,6 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let show_git_gutter = layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.show_git_diff_gutter
|
||||
.unwrap_or_else(|| {
|
||||
matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
)
|
||||
});
|
||||
if show_git_gutter {
|
||||
Self::paint_gutter_diff_hunks(layout, window, cx)
|
||||
}
|
||||
|
||||
let highlight_width = 0.275 * layout.position_map.line_height;
|
||||
let highlight_corner_radii = Corners::all(0.05 * layout.position_map.line_height);
|
||||
window.paint_layer(layout.gutter_hitbox.bounds, |window| {
|
||||
@@ -6879,7 +7059,8 @@ impl EditorElement {
|
||||
layout.width
|
||||
}
|
||||
|
||||
fn max_line_number_width(
|
||||
/// Get the width of the longest line number in the current editor in Pixels
|
||||
pub(crate) fn max_line_number_width(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
window: &mut Window,
|
||||
@@ -6974,7 +7155,7 @@ impl AcceptEditPredictionBinding {
|
||||
}
|
||||
|
||||
fn prepaint_gutter_button(
|
||||
button: IconButton,
|
||||
button: impl IntoElement,
|
||||
row: DisplayRow,
|
||||
line_height: Pixels,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
@@ -8959,6 +9140,10 @@ impl Element for EditorElement {
|
||||
self.paint_background(layout, window, cx);
|
||||
self.paint_indent_guides(layout, window, cx);
|
||||
|
||||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||||
self.paint_gutter_highlights(layout, window, cx);
|
||||
self.paint_gutter_indicators(layout, window, cx);
|
||||
}
|
||||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||||
self.paint_blamed_display_rows(layout, window, cx);
|
||||
self.paint_line_numbers(layout, window, cx);
|
||||
@@ -8966,11 +9151,6 @@ impl Element for EditorElement {
|
||||
|
||||
self.paint_text(layout, window, cx);
|
||||
|
||||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||||
self.paint_gutter_highlights(layout, window, cx);
|
||||
self.paint_gutter_indicators(layout, window, cx);
|
||||
}
|
||||
|
||||
if !layout.blocks.is_empty() {
|
||||
window.with_element_namespace("blocks", |window| {
|
||||
self.paint_blocks(layout, window, cx);
|
||||
|
||||
208
crates/editor/src/gutter/breakpoint_indicator.rs
Normal file
208
crates/editor/src/gutter/breakpoint_indicator.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use gpui::{Bounds, Path, PathBuilder, PathStyle, StrokeOptions, point};
|
||||
use ui::{Pixels, px};
|
||||
|
||||
/// Draw the path for the breakpoint indicator.
|
||||
///
|
||||
/// Note: The indicator needs to be a minimum of MIN_WIDTH px wide.
|
||||
/// wide to draw without graphical issues, so it will ignore narrower width.
|
||||
pub(crate) fn breakpoint_indicator_path(
|
||||
bounds: Bounds<Pixels>,
|
||||
scale: f32,
|
||||
stroke: bool,
|
||||
) -> Path<Pixels> {
|
||||
// Constants for the breakpoint shape dimensions
|
||||
// The shape is designed based on a 50px wide by 15px high template
|
||||
// and uses 9-slice style scaling to allow the shape to be stretched
|
||||
// vertically and horizontally.
|
||||
const SHAPE_BASE_HEIGHT: f32 = 15.0;
|
||||
const SHAPE_FIXED_WIDTH: f32 = 32.0; // Width of non-stretchable parts (corners)
|
||||
const SHAPE_MIN_WIDTH: f32 = 34.0; // Minimum width to render properly
|
||||
const PIXEL_ROUNDING_FACTOR: f32 = 8.0; // Round to nearest 1/8 pixel
|
||||
|
||||
// Key points in the shape (in base coordinates)
|
||||
const CORNER_RADIUS: f32 = 5.0;
|
||||
const CENTER_Y: f32 = 7.5;
|
||||
const TOP_Y: f32 = 0.0;
|
||||
const BOTTOM_Y: f32 = 15.0;
|
||||
const CURVE_CONTROL_OFFSET: f32 = 1.5;
|
||||
const RIGHT_CORNER_START: f32 = 4.0;
|
||||
const RIGHT_CORNER_WIDTH: f32 = 13.0;
|
||||
|
||||
// Helper function to round pixels to nearest 1/8
|
||||
let round_to_pixel_grid = |value: Pixels| -> Pixels {
|
||||
let value_f32: f32 = value.into();
|
||||
px((value_f32 * PIXEL_ROUNDING_FACTOR).round() / PIXEL_ROUNDING_FACTOR)
|
||||
};
|
||||
|
||||
// Calculate actual dimensions with scaling
|
||||
let min_allowed_width = px(SHAPE_MIN_WIDTH * scale);
|
||||
let actual_width = if bounds.size.width < min_allowed_width {
|
||||
min_allowed_width
|
||||
} else {
|
||||
bounds.size.width
|
||||
};
|
||||
let actual_height = bounds.size.height;
|
||||
|
||||
// Debug input parameters and initial calculations
|
||||
dbg!(&bounds);
|
||||
dbg!(scale);
|
||||
dbg!(stroke);
|
||||
dbg!(min_allowed_width);
|
||||
dbg!(actual_width);
|
||||
dbg!(actual_height);
|
||||
|
||||
// Origin point for positioning
|
||||
let origin_x = bounds.origin.x;
|
||||
let origin_y = bounds.origin.y;
|
||||
|
||||
// Calculate the scale factor based on height and user scale
|
||||
let shape_scale = (actual_height / px(SHAPE_BASE_HEIGHT)) * scale;
|
||||
|
||||
// Calculate the width of fixed and stretchable sections
|
||||
let fixed_sections_width = px(SHAPE_FIXED_WIDTH) * shape_scale;
|
||||
let stretchable_middle_width = actual_width - fixed_sections_width;
|
||||
|
||||
// Debug scaling calculations
|
||||
dbg!(shape_scale);
|
||||
dbg!(fixed_sections_width);
|
||||
dbg!(stretchable_middle_width);
|
||||
|
||||
// Pre-calculate all the key x-coordinates
|
||||
let left_edge_x = round_to_pixel_grid(origin_x);
|
||||
let left_corner_end_x = round_to_pixel_grid(origin_x + px(CORNER_RADIUS) * shape_scale);
|
||||
let middle_section_end_x =
|
||||
round_to_pixel_grid(origin_x + px(CORNER_RADIUS) * shape_scale + stretchable_middle_width);
|
||||
let right_corner_start_x = round_to_pixel_grid(
|
||||
origin_x
|
||||
+ px(CORNER_RADIUS) * shape_scale
|
||||
+ stretchable_middle_width
|
||||
+ px(RIGHT_CORNER_START) * shape_scale,
|
||||
);
|
||||
let right_edge_x = round_to_pixel_grid(
|
||||
origin_x
|
||||
+ px(CORNER_RADIUS) * shape_scale
|
||||
+ stretchable_middle_width
|
||||
+ px(RIGHT_CORNER_WIDTH) * shape_scale,
|
||||
);
|
||||
|
||||
// Debug x-coordinates
|
||||
dbg!(origin_x);
|
||||
dbg!(left_edge_x);
|
||||
dbg!(left_corner_end_x);
|
||||
dbg!(middle_section_end_x);
|
||||
dbg!(right_corner_start_x);
|
||||
dbg!(right_edge_x);
|
||||
|
||||
// Pre-calculate all the key y-coordinates
|
||||
let top_edge_y = round_to_pixel_grid(origin_y);
|
||||
let center_y = round_to_pixel_grid(origin_y + px(CENTER_Y) * shape_scale);
|
||||
let bottom_edge_y = round_to_pixel_grid(origin_y + px(BOTTOM_Y) * shape_scale);
|
||||
|
||||
// Y-coordinates for the left side curves
|
||||
let left_upper_curve_start_y = round_to_pixel_grid(origin_y + px(CORNER_RADIUS) * shape_scale);
|
||||
let left_lower_curve_end_y = round_to_pixel_grid(origin_y + px(10.0) * shape_scale);
|
||||
|
||||
// Y-coordinates for the right side curves
|
||||
let right_upper_curve_control_y = round_to_pixel_grid(origin_y + px(6.0) * shape_scale);
|
||||
let right_lower_curve_control_y = round_to_pixel_grid(origin_y + px(9.0) * shape_scale);
|
||||
|
||||
// Control point offsets
|
||||
let control_offset = px(CURVE_CONTROL_OFFSET) * shape_scale;
|
||||
let right_control_offset = px(9.0) * shape_scale;
|
||||
|
||||
// Debug y-coordinates
|
||||
dbg!(origin_y);
|
||||
dbg!(top_edge_y);
|
||||
dbg!(center_y);
|
||||
dbg!(bottom_edge_y);
|
||||
dbg!(left_upper_curve_start_y);
|
||||
dbg!(left_lower_curve_end_y);
|
||||
dbg!(right_upper_curve_control_y);
|
||||
dbg!(right_lower_curve_control_y);
|
||||
|
||||
// Create the path builder
|
||||
let mut builder = if stroke {
|
||||
let stroke_width = px(1.0 * scale);
|
||||
let options = StrokeOptions::default().with_line_width(stroke_width.0);
|
||||
PathBuilder::stroke(stroke_width).with_style(PathStyle::Stroke(options))
|
||||
} else {
|
||||
PathBuilder::fill()
|
||||
};
|
||||
|
||||
// Build the path - starting from left center
|
||||
builder.move_to(point(left_edge_x, center_y));
|
||||
|
||||
// === Upper half of the shape ===
|
||||
|
||||
// Move up to start of left upper curve
|
||||
builder.line_to(point(left_edge_x, left_upper_curve_start_y));
|
||||
|
||||
// Top-left corner curve
|
||||
builder.cubic_bezier_to(
|
||||
point(left_corner_end_x, top_edge_y),
|
||||
point(left_edge_x, round_to_pixel_grid(origin_y + control_offset)),
|
||||
point(round_to_pixel_grid(origin_x + control_offset), top_edge_y),
|
||||
);
|
||||
|
||||
// Top edge - stretchable middle section
|
||||
builder.line_to(point(middle_section_end_x, top_edge_y));
|
||||
|
||||
// Top edge - right corner start
|
||||
builder.line_to(point(right_corner_start_x, top_edge_y));
|
||||
|
||||
// Top-right corner curve
|
||||
builder.cubic_bezier_to(
|
||||
point(right_edge_x, center_y),
|
||||
point(
|
||||
round_to_pixel_grid(
|
||||
origin_x
|
||||
+ px(CORNER_RADIUS) * shape_scale
|
||||
+ stretchable_middle_width
|
||||
+ right_control_offset,
|
||||
),
|
||||
top_edge_y,
|
||||
),
|
||||
point(right_edge_x, right_upper_curve_control_y),
|
||||
);
|
||||
|
||||
// === Lower half of the shape (mirrored) ===
|
||||
|
||||
// Bottom-right corner curve
|
||||
builder.cubic_bezier_to(
|
||||
point(right_corner_start_x, bottom_edge_y),
|
||||
point(right_edge_x, right_lower_curve_control_y),
|
||||
point(
|
||||
round_to_pixel_grid(
|
||||
origin_x
|
||||
+ px(CORNER_RADIUS) * shape_scale
|
||||
+ stretchable_middle_width
|
||||
+ right_control_offset,
|
||||
),
|
||||
bottom_edge_y,
|
||||
),
|
||||
);
|
||||
|
||||
// Bottom edge - right corner to middle
|
||||
builder.line_to(point(middle_section_end_x, bottom_edge_y));
|
||||
|
||||
// Bottom edge - stretchable middle section
|
||||
builder.line_to(point(left_corner_end_x, bottom_edge_y));
|
||||
|
||||
// Bottom-left corner curve
|
||||
builder.cubic_bezier_to(
|
||||
point(left_edge_x, left_lower_curve_end_y),
|
||||
point(
|
||||
round_to_pixel_grid(origin_x + control_offset),
|
||||
bottom_edge_y,
|
||||
),
|
||||
point(
|
||||
left_edge_x,
|
||||
round_to_pixel_grid(origin_y + px(13.5) * shape_scale),
|
||||
),
|
||||
);
|
||||
|
||||
// Close the path by returning to start
|
||||
builder.line_to(point(left_edge_x, center_y));
|
||||
|
||||
builder.build().unwrap()
|
||||
}
|
||||
1
crates/editor/src/gutter/mod.rs
Normal file
1
crates/editor/src/gutter/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod breakpoint_indicator;
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::{
|
||||
Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder,
|
||||
PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowOptions, canvas,
|
||||
div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size,
|
||||
PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowOptions, bounds,
|
||||
canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size,
|
||||
};
|
||||
|
||||
struct PaintingViewer {
|
||||
@@ -150,6 +150,14 @@ impl PaintingViewer {
|
||||
let path = builder.build().unwrap();
|
||||
lines.push((path, gpui::green().into()));
|
||||
|
||||
// draw the indicators (aligned and unaligned versions)
|
||||
let aligned_indicator = breakpoint_indicator_path(
|
||||
bounds(point(px(50.), px(250.)), size(px(60.), px(16.))),
|
||||
1.0,
|
||||
false,
|
||||
);
|
||||
lines.push((aligned_indicator, rgb(0x1e88e5).into()));
|
||||
|
||||
Self {
|
||||
default_lines: lines.clone(),
|
||||
lines: vec![],
|
||||
@@ -306,3 +314,137 @@ fn main() {
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw the path for the breakpoint indicator.
|
||||
///
|
||||
/// Note: The indicator needs to be a minimum of MIN_WIDTH px wide.
|
||||
/// wide to draw without graphical issues, so it will ignore narrower width.
|
||||
fn breakpoint_indicator_path(bounds: Bounds<Pixels>, scale: f32, stroke: bool) -> Path<Pixels> {
|
||||
static MIN_WIDTH: f32 = 31.;
|
||||
|
||||
// Apply user scale to the minimum width
|
||||
let min_width = MIN_WIDTH * scale;
|
||||
|
||||
let width = if bounds.size.width.0 < min_width {
|
||||
px(min_width)
|
||||
} else {
|
||||
bounds.size.width
|
||||
};
|
||||
let height = bounds.size.height;
|
||||
|
||||
// Position the indicator on the canvas
|
||||
let base_x = bounds.origin.x;
|
||||
let base_y = bounds.origin.y;
|
||||
|
||||
// Calculate the scaling factor for the height (SVG is 15px tall), incorporating user scale
|
||||
let scale_factor = (height / px(15.0)) * scale;
|
||||
|
||||
// Calculate how much width to allocate to the stretchable middle section
|
||||
// SVG has 32px of fixed elements (corners), so the rest is for the middle
|
||||
let fixed_width = px(32.0) * scale_factor;
|
||||
let middle_width = width - fixed_width;
|
||||
|
||||
// Helper function to round to nearest quarter pixel
|
||||
let round_to_quarter = |value: Pixels| -> Pixels {
|
||||
let value_f32: f32 = value.into();
|
||||
px((value_f32 * 4.0).round() / 4.0)
|
||||
};
|
||||
|
||||
// Create a new path - either fill or stroke based on the flag
|
||||
let mut builder = if stroke {
|
||||
// For stroke, we need to set appropriate line width and options
|
||||
let stroke_width = px(1.0 * scale); // Apply scale to stroke width
|
||||
let options = StrokeOptions::default().with_line_width(stroke_width.0);
|
||||
|
||||
PathBuilder::stroke(stroke_width).with_style(PathStyle::Stroke(options))
|
||||
} else {
|
||||
// For fill, use the original implementation
|
||||
PathBuilder::fill()
|
||||
};
|
||||
|
||||
// Upper half of the shape - Based on the provided SVG
|
||||
// Start at bottom left (0, 8)
|
||||
let start_x = round_to_quarter(base_x);
|
||||
let start_y = round_to_quarter(base_y + px(7.5) * scale_factor);
|
||||
builder.move_to(point(start_x, start_y));
|
||||
|
||||
// Vertical line to (0, 5)
|
||||
let vert_y = round_to_quarter(base_y + px(5.0) * scale_factor);
|
||||
builder.line_to(point(start_x, vert_y));
|
||||
|
||||
// Curve to (5, 0) - using cubic Bezier
|
||||
let curve1_end_x = round_to_quarter(base_x + px(5.0) * scale_factor);
|
||||
let curve1_end_y = round_to_quarter(base_y);
|
||||
let curve1_ctrl1_x = round_to_quarter(base_x);
|
||||
let curve1_ctrl1_y = round_to_quarter(base_y + px(1.5) * scale_factor);
|
||||
let curve1_ctrl2_x = round_to_quarter(base_x + px(1.5) * scale_factor);
|
||||
let curve1_ctrl2_y = round_to_quarter(base_y);
|
||||
builder.cubic_bezier_to(
|
||||
point(curve1_end_x, curve1_end_y),
|
||||
point(curve1_ctrl1_x, curve1_ctrl1_y),
|
||||
point(curve1_ctrl2_x, curve1_ctrl2_y),
|
||||
);
|
||||
|
||||
// Horizontal line through the middle section to (37, 0)
|
||||
let middle_end_x = round_to_quarter(base_x + px(5.0) * scale_factor + middle_width);
|
||||
builder.line_to(point(middle_end_x, curve1_end_y));
|
||||
|
||||
// Horizontal line to (41, 0)
|
||||
let right_section_x =
|
||||
round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(4.0) * scale_factor);
|
||||
builder.line_to(point(right_section_x, curve1_end_y));
|
||||
|
||||
// Curve to (50, 7.5) - using cubic Bezier
|
||||
let curve2_end_x =
|
||||
round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(13.0) * scale_factor);
|
||||
let curve2_end_y = round_to_quarter(base_y + px(7.5) * scale_factor);
|
||||
let curve2_ctrl1_x =
|
||||
round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(9.0) * scale_factor);
|
||||
let curve2_ctrl1_y = round_to_quarter(base_y);
|
||||
let curve2_ctrl2_x =
|
||||
round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(13.0) * scale_factor);
|
||||
let curve2_ctrl2_y = round_to_quarter(base_y + px(6.0) * scale_factor);
|
||||
builder.cubic_bezier_to(
|
||||
point(curve2_end_x, curve2_end_y),
|
||||
point(curve2_ctrl1_x, curve2_ctrl1_y),
|
||||
point(curve2_ctrl2_x, curve2_ctrl2_y),
|
||||
);
|
||||
|
||||
// Lower half of the shape - mirrored vertically
|
||||
// Curve from (50, 7.5) to (41, 15)
|
||||
let curve3_end_y = round_to_quarter(base_y + px(15.0) * scale_factor);
|
||||
let curve3_ctrl1_x =
|
||||
round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(13.0) * scale_factor);
|
||||
let curve3_ctrl1_y = round_to_quarter(base_y + px(9.0) * scale_factor);
|
||||
let curve3_ctrl2_x =
|
||||
round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(9.0) * scale_factor);
|
||||
let curve3_ctrl2_y = round_to_quarter(base_y + px(15.0) * scale_factor);
|
||||
builder.cubic_bezier_to(
|
||||
point(right_section_x, curve3_end_y),
|
||||
point(curve3_ctrl1_x, curve3_ctrl1_y),
|
||||
point(curve3_ctrl2_x, curve3_ctrl2_y),
|
||||
);
|
||||
|
||||
// Horizontal line to (37, 15)
|
||||
builder.line_to(point(middle_end_x, curve3_end_y));
|
||||
|
||||
// Horizontal line through the middle section to (5, 15)
|
||||
builder.line_to(point(curve1_end_x, curve3_end_y));
|
||||
|
||||
// Curve to (0, 10)
|
||||
let curve4_end_y = round_to_quarter(base_y + px(10.0) * scale_factor);
|
||||
let curve4_ctrl1_x = round_to_quarter(base_x + px(1.5) * scale_factor);
|
||||
let curve4_ctrl1_y = round_to_quarter(base_y + px(15.0) * scale_factor);
|
||||
let curve4_ctrl2_x = round_to_quarter(base_x);
|
||||
let curve4_ctrl2_y = round_to_quarter(base_y + px(13.5) * scale_factor);
|
||||
builder.cubic_bezier_to(
|
||||
point(start_x, curve4_end_y),
|
||||
point(curve4_ctrl1_x, curve4_ctrl1_y),
|
||||
point(curve4_ctrl2_x, curve4_ctrl2_y),
|
||||
);
|
||||
|
||||
// Close the path
|
||||
builder.line_to(point(start_x, start_y));
|
||||
|
||||
builder.build().unwrap()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user