DOCK WORKING!

Update editor element to use mouse regions instead of dispatch event for mouse events
Fix bug in presenter where mouse region handlers were stored on click and called instead of more up to date handlers from subsequent renders
Changed MouseRegion to require discriminants in all cases
Add scroll wheel event to MouseRegion
Polished a bunch of dock inconsistencies

Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
This commit is contained in:
K Simmons 2022-09-08 19:32:38 -07:00
parent 59fd967793
commit 69ecbb644d
18 changed files with 618 additions and 498 deletions

View file

@ -32,6 +32,16 @@
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// Where to place the dock by default. This setting can take three
// values:
//
// 1. Position the dock attached to the bottom of the workspace
// "default_dock_anchor": "bottom"
// 2. Position the dock to the right of the workspace like a side panel
// "default_dock_anchor": "right"
// 3. Position the dock full screen over the entire workspace"
// "default_dock_anchor": "expanded"
"default_dock_anchor": "right",
// How to auto-format modified buffers when saving them. This
// setting can take three values:
//

View file

@ -28,7 +28,7 @@ use gpui::{
text_layout::{self, Line, RunStyle, TextLayoutCache},
AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext,
LayoutContext, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent,
MutableAppContext, PaintContext, Quad, Scene, ScrollWheelEvent, SizeConstraint, ViewContext,
MouseRegion, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext,
WeakViewHandle,
};
use json::json;
@ -41,6 +41,7 @@ use std::{
fmt::Write,
iter,
ops::Range,
sync::Arc,
};
const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
@ -76,9 +77,10 @@ impl SelectionLayout {
}
}
#[derive(Clone)]
pub struct EditorElement {
view: WeakViewHandle<Editor>,
style: EditorStyle,
style: Arc<EditorStyle>,
cursor_shape: CursorShape,
}
@ -90,7 +92,7 @@ impl EditorElement {
) -> Self {
Self {
view,
style,
style: Arc::new(style),
cursor_shape,
}
}
@ -110,8 +112,98 @@ impl EditorElement {
self.update_view(cx, |view, cx| view.snapshot(cx))
}
fn attach_mouse_handlers(
view: &WeakViewHandle<Editor>,
position_map: &Arc<PositionMap>,
visible_bounds: RectF,
text_bounds: RectF,
gutter_bounds: RectF,
bounds: RectF,
cx: &mut PaintContext,
) {
enum EditorElementMouseHandlers {}
cx.scene.push_mouse_region(
MouseRegion::new::<EditorElementMouseHandlers>(view.id(), view.id(), visible_bounds)
.on_down(MouseButton::Left, {
let position_map = position_map.clone();
move |e, cx| {
if !Self::mouse_down(
e.platform_event,
position_map.as_ref(),
text_bounds,
gutter_bounds,
cx,
) {
cx.propogate_event();
}
}
})
.on_down(MouseButton::Right, {
let position_map = position_map.clone();
move |e, cx| {
if !Self::mouse_right_down(
e.position,
position_map.as_ref(),
text_bounds,
cx,
) {
cx.propogate_event();
}
}
})
.on_up(MouseButton::Left, {
let view = view.clone();
let position_map = position_map.clone();
move |e, cx| {
if !Self::mouse_up(
view.clone(),
e.position,
e.cmd,
e.shift,
position_map.as_ref(),
text_bounds,
cx,
) {
cx.propogate_event()
}
}
})
.on_drag(MouseButton::Left, {
let view = view.clone();
let position_map = position_map.clone();
move |e, cx| {
if !Self::mouse_dragged(
view.clone(),
e.platform_event,
position_map.as_ref(),
text_bounds,
cx,
) {
cx.propogate_event()
}
}
})
.on_move({
let position_map = position_map.clone();
move |e, cx| {
if !Self::mouse_moved(e.platform_event, &position_map, text_bounds, cx) {
cx.propogate_event()
}
}
})
.on_scroll({
let position_map = position_map.clone();
move |e, cx| {
if !Self::scroll(e.position, e.delta, e.precise, &position_map, bounds, cx)
{
cx.propogate_event()
}
}
}),
);
}
fn mouse_down(
&self,
MouseButtonEvent {
position,
ctrl,
@ -121,18 +213,18 @@ impl EditorElement {
mut click_count,
..
}: MouseButtonEvent,
layout: &mut LayoutState,
paint: &mut PaintState,
position_map: &PositionMap,
text_bounds: RectF,
gutter_bounds: RectF,
cx: &mut EventContext,
) -> bool {
if paint.gutter_bounds.contains_point(position) {
if gutter_bounds.contains_point(position) {
click_count = 3; // Simulate triple-click when clicking the gutter to select lines
} else if !paint.text_bounds.contains_point(position) {
} else if !text_bounds.contains_point(position) {
return false;
}
let snapshot = self.snapshot(cx.app);
let (position, target_position) = paint.point_for_position(&snapshot, layout, position);
let (position, target_position) = position_map.point_for_position(text_bounds, position);
if shift && alt {
cx.dispatch_action(Select(SelectPhase::BeginColumnar {
@ -156,33 +248,31 @@ impl EditorElement {
}
fn mouse_right_down(
&self,
position: Vector2F,
layout: &mut LayoutState,
paint: &mut PaintState,
position_map: &PositionMap,
text_bounds: RectF,
cx: &mut EventContext,
) -> bool {
if !paint.text_bounds.contains_point(position) {
if !text_bounds.contains_point(position) {
return false;
}
let snapshot = self.snapshot(cx.app);
let (point, _) = paint.point_for_position(&snapshot, layout, position);
let (point, _) = position_map.point_for_position(text_bounds, position);
cx.dispatch_action(DeployMouseContextMenu { position, point });
true
}
fn mouse_up(
&self,
view: WeakViewHandle<Editor>,
position: Vector2F,
cmd: bool,
shift: bool,
layout: &mut LayoutState,
paint: &mut PaintState,
position_map: &PositionMap,
text_bounds: RectF,
cx: &mut EventContext,
) -> bool {
let view = self.view(cx.app.as_ref());
let view = view.upgrade(cx.app).unwrap().read(cx.app);
let end_selection = view.has_pending_selection();
let pending_nonempty_selections = view.has_pending_nonempty_selection();
@ -190,9 +280,8 @@ impl EditorElement {
cx.dispatch_action(Select(SelectPhase::End));
}
if !pending_nonempty_selections && cmd && paint.text_bounds.contains_point(position) {
let (point, target_point) =
paint.point_for_position(&self.snapshot(cx), layout, position);
if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
let (point, target_point) = position_map.point_for_position(text_bounds, position);
if point == target_point {
if shift {
@ -209,22 +298,21 @@ impl EditorElement {
}
fn mouse_dragged(
&self,
view: WeakViewHandle<Editor>,
MouseMovedEvent {
cmd,
shift,
position,
..
}: MouseMovedEvent,
layout: &mut LayoutState,
paint: &mut PaintState,
position_map: &PositionMap,
text_bounds: RectF,
cx: &mut EventContext,
) -> bool {
// This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
// Don't trigger hover popover if mouse is hovering over context menu
let point = if paint.text_bounds.contains_point(position) {
let (point, target_point) =
paint.point_for_position(&self.snapshot(cx), layout, position);
let point = if text_bounds.contains_point(position) {
let (point, target_point) = position_map.point_for_position(text_bounds, position);
if point == target_point {
Some(point)
} else {
@ -240,14 +328,13 @@ impl EditorElement {
shift_held: shift,
});
let view = self.view(cx.app);
let view = view.upgrade(cx.app).unwrap().read(cx.app);
if view.has_pending_selection() {
let rect = paint.text_bounds;
let mut scroll_delta = Vector2F::zero();
let vertical_margin = layout.line_height.min(rect.height() / 3.0);
let top = rect.origin_y() + vertical_margin;
let bottom = rect.lower_left().y() - vertical_margin;
let vertical_margin = position_map.line_height.min(text_bounds.height() / 3.0);
let top = text_bounds.origin_y() + vertical_margin;
let bottom = text_bounds.lower_left().y() - vertical_margin;
if position.y() < top {
scroll_delta.set_y(-scale_vertical_mouse_autoscroll_delta(top - position.y()))
}
@ -255,9 +342,9 @@ impl EditorElement {
scroll_delta.set_y(scale_vertical_mouse_autoscroll_delta(position.y() - bottom))
}
let horizontal_margin = layout.line_height.min(rect.width() / 3.0);
let left = rect.origin_x() + horizontal_margin;
let right = rect.upper_right().x() - horizontal_margin;
let horizontal_margin = position_map.line_height.min(text_bounds.width() / 3.0);
let left = text_bounds.origin_x() + horizontal_margin;
let right = text_bounds.upper_right().x() - horizontal_margin;
if position.x() < left {
scroll_delta.set_x(-scale_horizontal_mouse_autoscroll_delta(
left - position.x(),
@ -269,14 +356,14 @@ impl EditorElement {
))
}
let snapshot = self.snapshot(cx.app);
let (position, target_position) = paint.point_for_position(&snapshot, layout, position);
let (position, target_position) =
position_map.point_for_position(text_bounds, position);
cx.dispatch_action(Select(SelectPhase::Update {
position,
goal_column: target_position.column(),
scroll_position: (snapshot.scroll_position() + scroll_delta)
.clamp(Vector2F::zero(), layout.scroll_max),
scroll_position: (position_map.snapshot.scroll_position() + scroll_delta)
.clamp(Vector2F::zero(), position_map.scroll_max),
}));
cx.dispatch_action(HoverAt { point });
@ -288,22 +375,20 @@ impl EditorElement {
}
fn mouse_moved(
&self,
MouseMovedEvent {
cmd,
shift,
position,
..
}: MouseMovedEvent,
layout: &LayoutState,
paint: &PaintState,
position_map: &PositionMap,
text_bounds: RectF,
cx: &mut EventContext,
) -> bool {
// This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
// Don't trigger hover popover if mouse is hovering over context menu
let point = if paint.text_bounds.contains_point(position) {
let (point, target_point) =
paint.point_for_position(&self.snapshot(cx), layout, position);
let point = if text_bounds.contains_point(position) {
let (point, target_point) = position_map.point_for_position(text_bounds, position);
if point == target_point {
Some(point)
} else {
@ -319,23 +404,6 @@ impl EditorElement {
shift_held: shift,
});
if paint
.context_menu_bounds
.map_or(false, |context_menu_bounds| {
context_menu_bounds.contains_point(position)
})
{
return false;
}
if paint
.hover_popover_bounds
.iter()
.any(|hover_bounds| hover_bounds.contains_point(position))
{
return false;
}
cx.dispatch_action(HoverAt { point });
true
}
@ -349,28 +417,27 @@ impl EditorElement {
}
fn scroll(
&self,
position: Vector2F,
mut delta: Vector2F,
precise: bool,
layout: &mut LayoutState,
paint: &mut PaintState,
position_map: &PositionMap,
bounds: RectF,
cx: &mut EventContext,
) -> bool {
if !paint.bounds.contains_point(position) {
if !bounds.contains_point(position) {
return false;
}
let snapshot = self.snapshot(cx.app);
let max_glyph_width = layout.em_width;
let max_glyph_width = position_map.em_width;
if !precise {
delta *= vec2f(max_glyph_width, layout.line_height);
delta *= vec2f(max_glyph_width, position_map.line_height);
}
let scroll_position = snapshot.scroll_position();
let scroll_position = position_map.snapshot.scroll_position();
let x = (scroll_position.x() * max_glyph_width - delta.x()) / max_glyph_width;
let y = (scroll_position.y() * layout.line_height - delta.y()) / layout.line_height;
let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), layout.scroll_max);
let y =
(scroll_position.y() * position_map.line_height - delta.y()) / position_map.line_height;
let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), position_map.scroll_max);
cx.dispatch_action(Scroll(scroll_position));
@ -385,7 +452,8 @@ impl EditorElement {
cx: &mut PaintContext,
) {
let bounds = gutter_bounds.union_rect(text_bounds);
let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
let scroll_top =
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
let editor = self.view(cx.app);
cx.scene.push_quad(Quad {
bounds: gutter_bounds,
@ -414,11 +482,12 @@ impl EditorElement {
if !contains_non_empty_selection {
let origin = vec2f(
bounds.origin_x(),
bounds.origin_y() + (layout.line_height * *start_row as f32) - scroll_top,
bounds.origin_y() + (layout.position_map.line_height * *start_row as f32)
- scroll_top,
);
let size = vec2f(
bounds.width(),
layout.line_height * (end_row - start_row + 1) as f32,
layout.position_map.line_height * (end_row - start_row + 1) as f32,
);
cx.scene.push_quad(Quad {
bounds: RectF::new(origin, size),
@ -432,12 +501,13 @@ impl EditorElement {
if let Some(highlighted_rows) = &layout.highlighted_rows {
let origin = vec2f(
bounds.origin_x(),
bounds.origin_y() + (layout.line_height * highlighted_rows.start as f32)
bounds.origin_y()
+ (layout.position_map.line_height * highlighted_rows.start as f32)
- scroll_top,
);
let size = vec2f(
bounds.width(),
layout.line_height * highlighted_rows.len() as f32,
layout.position_map.line_height * highlighted_rows.len() as f32,
);
cx.scene.push_quad(Quad {
bounds: RectF::new(origin, size),
@ -456,23 +526,30 @@ impl EditorElement {
layout: &mut LayoutState,
cx: &mut PaintContext,
) {
let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
let scroll_top =
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
for (ix, line) in layout.line_number_layouts.iter().enumerate() {
if let Some(line) = line {
let line_origin = bounds.origin()
+ vec2f(
bounds.width() - line.width() - layout.gutter_padding,
ix as f32 * layout.line_height - (scroll_top % layout.line_height),
ix as f32 * layout.position_map.line_height
- (scroll_top % layout.position_map.line_height),
);
line.paint(line_origin, visible_bounds, layout.line_height, cx);
line.paint(
line_origin,
visible_bounds,
layout.position_map.line_height,
cx,
);
}
}
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
let mut x = bounds.width() - layout.gutter_padding;
let mut y = *row as f32 * layout.line_height - scroll_top;
let mut y = *row as f32 * layout.position_map.line_height - scroll_top;
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
y += (layout.line_height - indicator.size().y()) / 2.;
y += (layout.position_map.line_height - indicator.size().y()) / 2.;
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
}
}
@ -488,11 +565,12 @@ impl EditorElement {
let view = self.view(cx.app);
let style = &self.style;
let local_replica_id = view.replica_id(cx);
let scroll_position = layout.snapshot.scroll_position();
let scroll_position = layout.position_map.snapshot.scroll_position();
let start_row = scroll_position.y() as u32;
let scroll_top = scroll_position.y() * layout.line_height;
let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
let max_glyph_width = layout.em_width;
let scroll_top = scroll_position.y() * layout.position_map.line_height;
let end_row =
((scroll_top + bounds.height()) / layout.position_map.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
let max_glyph_width = layout.position_map.em_width;
let scroll_left = scroll_position.x() * max_glyph_width;
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
@ -514,7 +592,7 @@ impl EditorElement {
end_row,
*color,
0.,
0.15 * layout.line_height,
0.15 * layout.position_map.line_height,
layout,
content_origin,
scroll_top,
@ -527,7 +605,7 @@ impl EditorElement {
let mut cursors = SmallVec::<[Cursor; 32]>::new();
for (replica_id, selections) in &layout.selections {
let selection_style = style.replica_selection_style(*replica_id);
let corner_radius = 0.15 * layout.line_height;
let corner_radius = 0.15 * layout.position_map.line_height;
for selection in selections {
self.paint_highlighted_range(
@ -548,50 +626,52 @@ impl EditorElement {
if view.show_local_cursors() || *replica_id != local_replica_id {
let cursor_position = selection.head;
if (start_row..end_row).contains(&cursor_position.row()) {
let cursor_row_layout =
&layout.line_layouts[(cursor_position.row() - start_row) as usize];
let cursor_row_layout = &layout.position_map.line_layouts
[(cursor_position.row() - start_row) as usize];
let cursor_column = cursor_position.column() as usize;
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
let mut block_width =
cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;
if block_width == 0.0 {
block_width = layout.em_width;
block_width = layout.position_map.em_width;
}
let block_text = if let CursorShape::Block = self.cursor_shape {
layout
.position_map
.snapshot
.chars_at(cursor_position)
.next()
.and_then(|character| {
let font_id =
cursor_row_layout.font_for_index(cursor_column)?;
let text = character.to_string();
let block_text =
if let CursorShape::Block = self.cursor_shape {
layout.snapshot.chars_at(cursor_position).next().and_then(
|character| {
let font_id =
cursor_row_layout.font_for_index(cursor_column)?;
let text = character.to_string();
Some(cx.text_layout_cache.layout_str(
&text,
cursor_row_layout.font_size(),
&[(
text.len(),
RunStyle {
font_id,
color: style.background,
underline: Default::default(),
},
)],
))
},
)
} else {
None
};
Some(cx.text_layout_cache.layout_str(
&text,
cursor_row_layout.font_size(),
&[(
text.len(),
RunStyle {
font_id,
color: style.background,
underline: Default::default(),
},
)],
))
})
} else {
None
};
let x = cursor_character_x - scroll_left;
let y = cursor_position.row() as f32 * layout.line_height - scroll_top;
let y = cursor_position.row() as f32 * layout.position_map.line_height
- scroll_top;
cursors.push(Cursor {
color: selection_style.cursor,
block_width,
origin: vec2f(x, y),
line_height: layout.line_height,
line_height: layout.position_map.line_height,
shape: self.cursor_shape,
block_text,
});
@ -602,13 +682,16 @@ impl EditorElement {
if let Some(visible_text_bounds) = bounds.intersection(visible_bounds) {
// Draw glyphs
for (ix, line) in layout.line_layouts.iter().enumerate() {
for (ix, line) in layout.position_map.line_layouts.iter().enumerate() {
let row = start_row + ix as u32;
line.paint(
content_origin
+ vec2f(-scroll_left, row as f32 * layout.line_height - scroll_top),
+ vec2f(
-scroll_left,
row as f32 * layout.position_map.line_height - scroll_top,
),
visible_text_bounds,
layout.line_height,
layout.position_map.line_height,
cx,
);
}
@ -622,9 +705,10 @@ impl EditorElement {
if let Some((position, context_menu)) = layout.context_menu.as_mut() {
cx.scene.push_stacking_context(None);
let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
let cursor_row_layout =
&layout.position_map.line_layouts[(position.row() - start_row) as usize];
let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
let y = (position.row() + 1) as f32 * layout.line_height - scroll_top;
let y = (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top;
let mut list_origin = content_origin + vec2f(x, y);
let list_width = context_menu.size().x();
let list_height = context_menu.size().y();
@ -636,7 +720,7 @@ impl EditorElement {
}
if list_origin.y() + list_height > bounds.max_y() {
list_origin.set_y(list_origin.y() - layout.line_height - list_height);
list_origin.set_y(list_origin.y() - layout.position_map.line_height - list_height);
}
context_menu.paint(
@ -654,18 +738,19 @@ impl EditorElement {
cx.scene.push_stacking_context(None);
// This is safe because we check on layout whether the required row is available
let hovered_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
let hovered_row_layout =
&layout.position_map.line_layouts[(position.row() - start_row) as usize];
// Minimum required size: Take the first popover, and add 1.5 times the minimum popover
// height. This is the size we will use to decide whether to render popovers above or below
// the hovered line.
let first_size = hover_popovers[0].size();
let height_to_reserve =
first_size.y() + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.line_height;
let height_to_reserve = first_size.y()
+ 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height;
// Compute Hovered Point
let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
let y = position.row() as f32 * layout.line_height - scroll_top;
let y = position.row() as f32 * layout.position_map.line_height - scroll_top;
let hovered_point = content_origin + vec2f(x, y);
paint.hover_popover_bounds.clear();
@ -697,7 +782,7 @@ impl EditorElement {
}
} else {
// There is not enough space above. Render popovers below the hovered point
let mut current_y = hovered_point.y() + layout.line_height;
let mut current_y = hovered_point.y() + layout.position_map.line_height;
for hover_popover in hover_popovers {
let size = hover_popover.size();
let mut popover_origin = vec2f(hovered_point.x(), current_y);
@ -753,14 +838,16 @@ impl EditorElement {
let highlighted_range = HighlightedRange {
color,
line_height: layout.line_height,
line_height: layout.position_map.line_height,
corner_radius,
start_y: content_origin.y() + row_range.start as f32 * layout.line_height
start_y: content_origin.y()
+ row_range.start as f32 * layout.position_map.line_height
- scroll_top,
lines: row_range
.into_iter()
.map(|row| {
let line_layout = &layout.line_layouts[(row - start_row) as usize];
let line_layout =
&layout.position_map.line_layouts[(row - start_row) as usize];
HighlightedRangeLine {
start_x: if row == range.start.row() {
content_origin.x()
@ -793,13 +880,16 @@ impl EditorElement {
layout: &mut LayoutState,
cx: &mut PaintContext,
) {
let scroll_position = layout.snapshot.scroll_position();
let scroll_left = scroll_position.x() * layout.em_width;
let scroll_top = scroll_position.y() * layout.line_height;
let scroll_position = layout.position_map.snapshot.scroll_position();
let scroll_left = scroll_position.x() * layout.position_map.em_width;
let scroll_top = scroll_position.y() * layout.position_map.line_height;
for block in &mut layout.blocks {
let mut origin =
bounds.origin() + vec2f(0., block.row as f32 * layout.line_height - scroll_top);
let mut origin = bounds.origin()
+ vec2f(
0.,
block.row as f32 * layout.position_map.line_height - scroll_top,
);
if !matches!(block.style, BlockStyle::Sticky) {
origin += vec2f(-scroll_left, 0.);
}
@ -1483,22 +1573,24 @@ impl Element for EditorElement {
(
size,
LayoutState {
size,
scroll_max,
position_map: Arc::new(PositionMap {
size,
scroll_max,
line_layouts,
line_height,
em_width,
em_advance,
snapshot,
}),
gutter_size,
gutter_padding,
text_size,
gutter_margin,
snapshot,
active_rows,
highlighted_rows,
highlighted_ranges,
line_layouts,
line_number_layouts,
blocks,
line_height,
em_width,
em_advance,
selections,
context_menu,
code_actions_indicator,
@ -1523,13 +1615,20 @@ impl Element for EditorElement {
);
let mut paint_state = PaintState {
bounds,
gutter_bounds,
text_bounds,
context_menu_bounds: None,
hover_popover_bounds: Default::default(),
};
Self::attach_mouse_handlers(
&self.view,
&layout.position_map,
visible_bounds,
text_bounds,
gutter_bounds,
bounds,
cx,
);
self.paint_background(gutter_bounds, text_bounds, layout, cx);
if layout.gutter_size.x() > 0. {
self.paint_gutter(gutter_bounds, visible_bounds, layout, cx);
@ -1552,78 +1651,15 @@ impl Element for EditorElement {
event: &Event,
_: RectF,
_: RectF,
layout: &mut LayoutState,
paint: &mut PaintState,
_: &mut LayoutState,
_: &mut PaintState,
cx: &mut EventContext,
) -> bool {
if let Some((_, context_menu)) = &mut layout.context_menu {
if context_menu.dispatch_event(event, cx) {
return true;
}
if let Event::ModifiersChanged(event) = event {
self.modifiers_changed(*event, cx);
}
if let Some((_, indicator)) = &mut layout.code_actions_indicator {
if indicator.dispatch_event(event, cx) {
return true;
}
}
if let Some((_, popover_elements)) = &mut layout.hover_popovers {
for popover_element in popover_elements.iter_mut() {
if popover_element.dispatch_event(event, cx) {
return true;
}
}
}
for block in &mut layout.blocks {
if block.element.dispatch_event(event, cx) {
return true;
}
}
match event {
&Event::MouseDown(
event @ MouseButtonEvent {
button: MouseButton::Left,
..
},
) => self.mouse_down(event, layout, paint, cx),
&Event::MouseDown(MouseButtonEvent {
button: MouseButton::Right,
position,
..
}) => self.mouse_right_down(position, layout, paint, cx),
&Event::MouseUp(MouseButtonEvent {
button: MouseButton::Left,
position,
cmd,
shift,
..
}) => self.mouse_up(position, cmd, shift, layout, paint, cx),
Event::MouseMoved(
event @ MouseMovedEvent {
pressed_button: Some(MouseButton::Left),
..
},
) => self.mouse_dragged(*event, layout, paint, cx),
Event::ScrollWheel(ScrollWheelEvent {
position,
delta,
precise,
..
}) => self.scroll(*position, *delta, *precise, layout, paint, cx),
&Event::ModifiersChanged(event) => self.modifiers_changed(event, cx),
&Event::MouseMoved(event) => self.mouse_moved(event, layout, paint, cx),
_ => false,
}
false
}
fn rect_for_text_range(
@ -1640,26 +1676,34 @@ impl Element for EditorElement {
layout.text_size,
);
let content_origin = text_bounds.origin() + vec2f(layout.gutter_margin, 0.);
let scroll_position = layout.snapshot.scroll_position();
let scroll_position = layout.position_map.snapshot.scroll_position();
let start_row = scroll_position.y() as u32;
let scroll_top = scroll_position.y() * layout.line_height;
let scroll_left = scroll_position.x() * layout.em_width;
let scroll_top = scroll_position.y() * layout.position_map.line_height;
let scroll_left = scroll_position.x() * layout.position_map.em_width;
let range_start =
OffsetUtf16(range_utf16.start).to_display_point(&layout.snapshot.display_snapshot);
let range_start = OffsetUtf16(range_utf16.start)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
if range_start.row() < start_row {
return None;
}
let line = layout
.position_map
.line_layouts
.get((range_start.row() - start_row) as usize)?;
let range_start_x = line.x_for_index(range_start.column() as usize);
let range_start_y = range_start.row() as f32 * layout.line_height;
let range_start_y = range_start.row() as f32 * layout.position_map.line_height;
Some(RectF::new(
content_origin + vec2f(range_start_x, range_start_y + layout.line_height)
content_origin
+ vec2f(
range_start_x,
range_start_y + layout.position_map.line_height,
)
- vec2f(scroll_left, scroll_top),
vec2f(layout.em_width, layout.line_height),
vec2f(
layout.position_map.em_width,
layout.position_map.line_height,
),
))
}
@ -1678,21 +1722,15 @@ impl Element for EditorElement {
}
pub struct LayoutState {
size: Vector2F,
scroll_max: Vector2F,
position_map: Arc<PositionMap>,
gutter_size: Vector2F,
gutter_padding: f32,
gutter_margin: f32,
text_size: Vector2F,
snapshot: EditorSnapshot,
active_rows: BTreeMap<u32, bool>,
highlighted_rows: Option<Range<u32>>,
line_layouts: Vec<text_layout::Line>,
line_number_layouts: Vec<Option<text_layout::Line>>,
blocks: Vec<BlockLayout>,
line_height: f32,
em_width: f32,
em_advance: f32,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
context_menu: Option<(DisplayPoint, ElementBox)>,
@ -1700,6 +1738,52 @@ pub struct LayoutState {
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
}
pub struct PositionMap {
size: Vector2F,
line_height: f32,
scroll_max: Vector2F,
em_width: f32,
em_advance: f32,
line_layouts: Vec<text_layout::Line>,
snapshot: EditorSnapshot,
}
impl PositionMap {
/// Returns two display points:
/// 1. The nearest *valid* position in the editor
/// 2. An unclipped, potentially *invalid* position that maps directly to
/// the given pixel position.
fn point_for_position(
&self,
text_bounds: RectF,
position: Vector2F,
) -> (DisplayPoint, DisplayPoint) {
let scroll_position = self.snapshot.scroll_position();
let position = position - text_bounds.origin();
let y = position.y().max(0.0).min(self.size.y());
let x = position.x() + (scroll_position.x() * self.em_width);
let row = (y / self.line_height + scroll_position.y()) as u32;
let (column, x_overshoot) = if let Some(line) = self
.line_layouts
.get(row as usize - scroll_position.y() as usize)
{
if let Some(ix) = line.index_for_x(x) {
(ix as u32, 0.0)
} else {
(line.len() as u32, 0f32.max(x - line.width()))
}
} else {
(0, x)
};
let mut target_point = DisplayPoint::new(row, column);
let point = self.snapshot.clip_point(target_point, Bias::Left);
*target_point.column_mut() += (x_overshoot / self.em_advance) as u32;
(point, target_point)
}
}
struct BlockLayout {
row: u32,
element: ElementBox,
@ -1738,50 +1822,10 @@ fn layout_line(
}
pub struct PaintState {
bounds: RectF,
gutter_bounds: RectF,
text_bounds: RectF,
context_menu_bounds: Option<RectF>,
hover_popover_bounds: Vec<RectF>,
}
impl PaintState {
/// Returns two display points:
/// 1. The nearest *valid* position in the editor
/// 2. An unclipped, potentially *invalid* position that maps directly to
/// the given pixel position.
fn point_for_position(
&self,
snapshot: &EditorSnapshot,
layout: &LayoutState,
position: Vector2F,
) -> (DisplayPoint, DisplayPoint) {
let scroll_position = snapshot.scroll_position();
let position = position - self.text_bounds.origin();
let y = position.y().max(0.0).min(layout.size.y());
let x = position.x() + (scroll_position.x() * layout.em_width);
let row = (y / layout.line_height + scroll_position.y()) as u32;
let (column, x_overshoot) = if let Some(line) = layout
.line_layouts
.get(row as usize - scroll_position.y() as usize)
{
if let Some(ix) = line.index_for_x(x) {
(ix as u32, 0.0)
} else {
(line.len() as u32, 0f32.max(x - line.width()))
}
} else {
(0, x)
};
let mut target_point = DisplayPoint::new(row, column);
let point = snapshot.clip_point(target_point, Bias::Left);
*target_point.column_mut() += (x_overshoot / layout.em_advance) as u32;
(point, target_point)
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum CursorShape {
Bar,
@ -2090,7 +2134,7 @@ mod tests {
&mut layout_cx,
);
assert_eq!(state.line_layouts.len(), 4);
assert_eq!(state.position_map.line_layouts.len(), 4);
assert_eq!(
state
.line_number_layouts

View file

@ -4028,7 +4028,7 @@ pub struct RenderParams {
pub view_id: usize,
pub titlebar_height: f32,
pub hovered_region_ids: HashSet<MouseRegionId>,
pub clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
pub refreshing: bool,
}
@ -4037,7 +4037,7 @@ pub struct RenderContext<'a, T: View> {
pub(crate) view_id: usize,
pub(crate) view_type: PhantomData<T>,
pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
pub(crate) clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
pub(crate) clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
pub app: &'a mut MutableAppContext,
pub titlebar_height: f32,
pub refreshing: bool,

View file

@ -1,7 +1,7 @@
use crate::{
geometry::vector::Vector2F, presenter::MeasurementContext, CursorRegion, DebugContext, Element,
ElementBox, Event, EventContext, LayoutContext, MouseButton, MouseButtonEvent, MouseRegion,
NavigationDirection, PaintContext, SizeConstraint,
geometry::vector::Vector2F, presenter::MeasurementContext, scene::HandlerSet, CursorRegion,
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseButton,
MouseButtonEvent, MouseRegion, NavigationDirection, PaintContext, SizeConstraint,
};
use pathfinder_geometry::rect::RectF;
use serde_json::json;
@ -82,11 +82,13 @@ impl Element for EventHandler {
bounds: visible_bounds,
style: Default::default(),
});
cx.scene.push_mouse_region(MouseRegion::handle_all(
cx.current_view_id(),
Some(discriminant),
visible_bounds,
));
cx.scene.push_mouse_region(MouseRegion {
view_id: cx.current_view_id(),
discriminant,
bounds: visible_bounds,
handlers: HandlerSet::capture_all(),
hoverable: true,
});
cx.scene.pop_stacking_context();
}
self.child.paint(bounds.origin(), visible_bounds, cx);

View file

@ -167,15 +167,13 @@ impl Element for MouseEventHandler {
});
}
cx.scene.push_mouse_region(
MouseRegion::from_handlers(
cx.current_view_id(),
Some(self.discriminant),
hit_bounds,
self.handlers.clone(),
)
.with_hoverable(self.hoverable),
);
cx.scene.push_mouse_region(MouseRegion {
view_id: cx.current_view_id(),
discriminant: self.discriminant,
bounds: hit_bounds,
handlers: self.handlers.clone(),
hoverable: self.hoverable,
});
self.child.paint(bounds.origin(), visible_bounds, cx);
}

View file

@ -1,4 +1,4 @@
use std::ops::Range;
use std::{any::TypeId, ops::Range};
use crate::{
geometry::{rect::RectF, vector::Vector2F},
@ -78,10 +78,13 @@ impl Element for Overlay {
cx.scene.push_stacking_context(None);
if self.hoverable {
enum OverlayHoverCapture {}
cx.scene.push_mouse_region(MouseRegion {
view_id: cx.current_view_id(),
bounds,
..Default::default()
discriminant: (TypeId::of::<OverlayHoverCapture>(), cx.current_view_id()),
handlers: Default::default(),
hoverable: true,
});
}

View file

@ -8,7 +8,8 @@ use crate::{
platform::{CursorStyle, Event},
scene::{
ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, ScrollWheelRegionEvent,
UpOutRegionEvent, UpRegionEvent,
},
text_layout::TextLayoutCache,
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity,
@ -36,7 +37,7 @@ pub struct Presenter {
asset_cache: Arc<AssetCache>,
last_mouse_moved_event: Option<Event>,
hovered_region_ids: HashSet<MouseRegionId>,
clicked_regions: Vec<MouseRegion>,
clicked_region_ids: HashSet<MouseRegionId>,
clicked_button: Option<MouseButton>,
mouse_position: Vector2F,
titlebar_height: f32,
@ -61,7 +62,7 @@ impl Presenter {
asset_cache,
last_mouse_moved_event: None,
hovered_region_ids: Default::default(),
clicked_regions: Vec::new(),
clicked_region_ids: Default::default(),
clicked_button: None,
mouse_position: vec2f(0., 0.),
titlebar_height,
@ -75,7 +76,6 @@ impl Presenter {
) {
cx.start_frame();
for view_id in &invalidation.removed {
invalidation.updated.remove(view_id);
self.rendered_views.remove(view_id);
}
for view_id in &invalidation.updated {
@ -86,15 +86,9 @@ impl Presenter {
view_id: *view_id,
titlebar_height: self.titlebar_height,
hovered_region_ids: self.hovered_region_ids.clone(),
clicked_region_ids: self.clicked_button.map(|button| {
(
self.clicked_regions
.iter()
.filter_map(MouseRegion::id)
.collect(),
button,
)
}),
clicked_region_ids: self
.clicked_button
.map(|button| (self.clicked_region_ids.clone(), button)),
refreshing: false,
})
.unwrap(),
@ -112,15 +106,9 @@ impl Presenter {
view_id: *view_id,
titlebar_height: self.titlebar_height,
hovered_region_ids: self.hovered_region_ids.clone(),
clicked_region_ids: self.clicked_button.map(|button| {
(
self.clicked_regions
.iter()
.filter_map(MouseRegion::id)
.collect(),
button,
)
}),
clicked_region_ids: self
.clicked_button
.map(|button| (self.clicked_region_ids.clone(), button)),
refreshing: true,
})
.unwrap();
@ -184,15 +172,9 @@ impl Presenter {
view_stack: Vec::new(),
refreshing,
hovered_region_ids: self.hovered_region_ids.clone(),
clicked_region_ids: self.clicked_button.map(|button| {
(
self.clicked_regions
.iter()
.filter_map(MouseRegion::id)
.collect(),
button,
)
}),
clicked_region_ids: self
.clicked_button
.map(|button| (self.clicked_region_ids.clone(), button)),
titlebar_height: self.titlebar_height,
window_size,
app: cx,
@ -248,14 +230,15 @@ impl Presenter {
// If there is already clicked_button stored, don't replace it.
if self.clicked_button.is_none() {
self.clicked_regions = self
self.clicked_region_ids = self
.mouse_regions
.iter()
.filter_map(|(region, _)| {
region
.bounds
.contains_point(e.position)
.then(|| region.clone())
if region.bounds.contains_point(e.position) {
Some(region.id())
} else {
None
}
})
.collect();
self.clicked_button = Some(e.button);
@ -341,6 +324,12 @@ impl Presenter {
self.last_mouse_moved_event = Some(event.clone());
}
Event::ScrollWheel(e) => {
events_to_send.push(MouseRegionEvent::ScrollWheel(ScrollWheelRegionEvent {
region: Default::default(),
platform_event: e.clone(),
}))
}
_ => {}
}
@ -375,23 +364,21 @@ impl Presenter {
top_most_depth = Some(depth);
}
if let Some(region_id) = region.id() {
// This unwrap relies on short circuiting boolean expressions
// The right side of the && is only executed when contains_mouse
// is true, and we know above that when contains_mouse is true
// top_most_depth is set
if contains_mouse && depth == top_most_depth.unwrap() {
//Ensure that hover entrance events aren't sent twice
if self.hovered_region_ids.insert(region_id) {
valid_regions.push(region.clone());
invalidated_views.insert(region.view_id);
}
} else {
// Ensure that hover exit events aren't sent twice
if self.hovered_region_ids.remove(&region_id) {
valid_regions.push(region.clone());
invalidated_views.insert(region.view_id);
}
// This unwrap relies on short circuiting boolean expressions
// The right side of the && is only executed when contains_mouse
// is true, and we know above that when contains_mouse is true
// top_most_depth is set
if contains_mouse && depth == top_most_depth.unwrap() {
//Ensure that hover entrance events aren't sent twice
if self.hovered_region_ids.insert(region.id()) {
valid_regions.push(region.clone());
invalidated_views.insert(region.view_id);
}
} else {
// Ensure that hover exit events aren't sent twice
if self.hovered_region_ids.remove(&region.id()) {
valid_regions.push(region.clone());
invalidated_views.insert(region.view_id);
}
}
}
@ -404,21 +391,23 @@ impl Presenter {
.unwrap_or(false)
{
// Clear clicked regions and clicked button
let clicked_regions =
std::mem::replace(&mut self.clicked_regions, Vec::new());
let clicked_region_ids =
std::mem::replace(&mut self.clicked_region_ids, Default::default());
self.clicked_button = None;
// Find regions which still overlap with the mouse since the last MouseDown happened
for clicked_region in clicked_regions.into_iter().rev() {
if clicked_region.bounds.contains_point(e.position) {
valid_regions.push(clicked_region);
for (mouse_region, _) in self.mouse_regions.iter().rev() {
if clicked_region_ids.contains(&mouse_region.id()) {
valid_regions.push(mouse_region.clone());
}
}
}
}
MouseRegionEvent::Drag(_) => {
for clicked_region in self.clicked_regions.iter().rev() {
valid_regions.push(clicked_region.clone());
for (mouse_region, _) in self.mouse_regions.iter().rev() {
if self.clicked_region_ids.contains(&mouse_region.id()) {
valid_regions.push(mouse_region.clone());
}
}
}
@ -447,10 +436,7 @@ impl Presenter {
region_event.set_region(valid_region.bounds);
if let MouseRegionEvent::Hover(e) = &mut region_event {
e.started = valid_region
.id()
.map(|region_id| hovered_region_ids.contains(&region_id))
.unwrap_or(false)
e.started = hovered_region_ids.contains(&valid_region.id())
}
// Handle Down events if the MouseRegion has a Click handler. This makes the api more intuitive as you would
// not expect a MouseRegion to be transparent to Down events if it also has a Click handler.
@ -546,7 +532,7 @@ pub struct LayoutContext<'a> {
pub window_size: Vector2F,
titlebar_height: f32,
hovered_region_ids: HashSet<MouseRegionId>,
clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
}
impl<'a> LayoutContext<'a> {

View file

@ -536,11 +536,11 @@ impl ToJson for Border {
}
impl MouseRegion {
pub fn id(&self) -> Option<MouseRegionId> {
self.discriminant.map(|discriminant| MouseRegionId {
pub fn id(&self) -> MouseRegionId {
MouseRegionId {
view_id: self.view_id,
discriminant,
})
discriminant: self.discriminant,
}
}
}

View file

@ -6,54 +6,51 @@ use pathfinder_geometry::rect::RectF;
use crate::{EventContext, MouseButton};
use super::mouse_region_event::{
ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent,
MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
use super::{
mouse_region_event::{
ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent,
MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
},
ScrollWheelRegionEvent,
};
#[derive(Clone, Default)]
#[derive(Clone)]
pub struct MouseRegion {
pub view_id: usize,
pub discriminant: Option<(TypeId, usize)>,
pub discriminant: (TypeId, usize),
pub bounds: RectF,
pub handlers: HandlerSet,
pub hoverable: bool,
}
impl MouseRegion {
pub fn new(view_id: usize, discriminant: Option<(TypeId, usize)>, bounds: RectF) -> Self {
Self::from_handlers(view_id, discriminant, bounds, Default::default())
/// Region ID is used to track semantically equivalent mouse regions across render passes.
/// e.g. if you have mouse handlers attached to a list item type, then each item of the list
/// should pass a different (consistent) region_id. If you have one big region that covers your
/// whole component, just pass the view_id again.
pub fn new<Tag: 'static>(view_id: usize, region_id: usize, bounds: RectF) -> Self {
Self::from_handlers::<Tag>(view_id, region_id, bounds, Default::default())
}
pub fn from_handlers(
pub fn handle_all<Tag: 'static>(view_id: usize, region_id: usize, bounds: RectF) -> Self {
Self::from_handlers::<Tag>(view_id, region_id, bounds, HandlerSet::capture_all())
}
pub fn from_handlers<Tag: 'static>(
view_id: usize,
discriminant: Option<(TypeId, usize)>,
region_id: usize,
bounds: RectF,
handlers: HandlerSet,
) -> Self {
Self {
view_id,
discriminant,
discriminant: (TypeId::of::<Tag>(), region_id),
bounds,
handlers,
hoverable: true,
}
}
pub fn handle_all(
view_id: usize,
discriminant: Option<(TypeId, usize)>,
bounds: RectF,
) -> Self {
Self {
view_id,
discriminant,
bounds,
handlers: HandlerSet::capture_all(),
hoverable: true,
}
}
pub fn on_down(
mut self,
button: MouseButton,
@ -124,6 +121,14 @@ impl MouseRegion {
self
}
pub fn on_scroll(
mut self,
handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
) -> Self {
self.handlers = self.handlers.on_scroll(handler);
self
}
pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
self.hoverable = is_hoverable;
self
@ -345,4 +350,22 @@ impl HandlerSet {
}));
self
}
pub fn on_scroll(
mut self,
handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
) -> Self {
self.set.insert((MouseRegionEvent::scroll_wheel_disc(), None),
Rc::new(move |region_event, cx| {
if let MouseRegionEvent::ScrollWheel(e) = region_event {
handler(e, cx);
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::ScrollWheel, found {:?}",
region_event
);
}
}));
self
}
}

View file

@ -168,7 +168,7 @@ impl MouseRegionEvent {
pub fn is_capturable(&self) -> bool {
match self {
MouseRegionEvent::Move(_) => true,
MouseRegionEvent::Drag(_) => false,
MouseRegionEvent::Drag(_) => true,
MouseRegionEvent::Hover(_) => false,
MouseRegionEvent::Down(_) => true,
MouseRegionEvent::Up(_) => true,

View file

@ -29,6 +29,7 @@ pub struct Settings {
pub show_completions_on_input: bool,
pub vim_mode: bool,
pub autosave: Autosave,
pub default_dock_anchor: DockAnchor,
pub editor_defaults: EditorSettings,
pub editor_overrides: EditorSettings,
pub terminal_defaults: TerminalSettings,
@ -150,6 +151,15 @@ pub enum WorkingDirectory {
Always { directory: String },
}
#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DockAnchor {
#[default]
Bottom,
Right,
Expanded,
}
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct SettingsFileContent {
pub experiments: Option<FeatureFlags>,
@ -167,6 +177,8 @@ pub struct SettingsFileContent {
pub vim_mode: Option<bool>,
#[serde(default)]
pub autosave: Option<Autosave>,
#[serde(default)]
pub default_dock_anchor: Option<DockAnchor>,
#[serde(flatten)]
pub editor: EditorSettings,
#[serde(default)]
@ -216,6 +228,7 @@ impl Settings {
projects_online_by_default: defaults.projects_online_by_default.unwrap(),
vim_mode: defaults.vim_mode.unwrap(),
autosave: defaults.autosave.unwrap(),
default_dock_anchor: defaults.default_dock_anchor.unwrap(),
editor_defaults: EditorSettings {
tab_size: required(defaults.editor.tab_size),
hard_tabs: required(defaults.editor.hard_tabs),
@ -268,6 +281,8 @@ impl Settings {
merge(&mut self.autosave, data.autosave);
merge(&mut self.experiments, data.experiments);
merge(&mut self.staff_mode, data.staff_mode);
merge(&mut self.default_dock_anchor, data.default_dock_anchor);
// Ensure terminal font is loaded, so we can request it in terminal_element layout
if let Some(terminal_font) = &data.terminal.font_family {
font_cache.load_family(&[terminal_font]).log_err();
@ -336,6 +351,7 @@ impl Settings {
show_completions_on_input: true,
vim_mode: false,
autosave: Autosave::Off,
default_dock_anchor: DockAnchor::Bottom,
editor_defaults: EditorSettings {
tab_size: Some(4.try_into().unwrap()),
hard_tabs: Some(false),

View file

@ -366,7 +366,7 @@ impl TerminalElement {
) {
let connection = self.terminal;
let mut region = MouseRegion::new(view_id, None, visible_bounds);
let mut region = MouseRegion::new::<Self>(view_id, view_id, visible_bounds);
// Terminal Emulator controlled behavior:
region = region

View file

@ -57,7 +57,7 @@ pub struct Workspace {
pub notifications: Notifications,
pub joining_project_avatar: ImageStyle,
pub joining_project_message: ContainedText,
pub fullscreen_dock: ContainerStyle,
pub dock: Dock,
}
#[derive(Clone, Deserialize, Default)]
@ -150,6 +150,14 @@ pub struct Toolbar {
pub nav_button: Interactive<IconButton>,
}
#[derive(Clone, Deserialize, Default)]
pub struct Dock {
pub wash_color: Color,
pub flex: f32,
pub panel: ContainerStyle,
pub maximized: ContainerStyle,
}
#[derive(Clone, Deserialize, Default)]
pub struct Notifications {
#[serde(flatten)]

View file

@ -1,14 +1,14 @@
use gpui::{
actions,
elements::{ChildView, MouseEventHandler, Svg},
elements::{ChildView, Container, FlexItem, Margin, MouseEventHandler, Svg},
impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton,
MutableAppContext, View, ViewContext, ViewHandle, WeakViewHandle,
MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
};
use serde::Deserialize;
use settings::Settings;
use settings::{DockAnchor, Settings};
use theme::Theme;
use crate::{pane, ItemHandle, Pane, StatusItemView, Workspace};
use crate::{ItemHandle, Pane, StatusItemView, Workspace};
#[derive(PartialEq, Clone, Deserialize)]
pub struct MoveDock(pub DockAnchor);
@ -24,14 +24,6 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Dock::move_dock);
}
#[derive(PartialEq, Eq, Default, Copy, Clone, Deserialize)]
pub enum DockAnchor {
#[default]
Bottom,
Right,
Expanded,
}
#[derive(Copy, Clone)]
pub enum DockPosition {
Shown(DockAnchor),
@ -79,63 +71,56 @@ pub struct Dock {
impl Dock {
pub fn new(cx: &mut ViewContext<Workspace>, default_item_factory: DefaultItemFactory) -> Self {
let pane = cx.add_view(|cx| Pane::new(true, cx));
cx.subscribe(&pane.clone(), |workspace, _, event, cx| {
if let pane::Event::Remove = event {
workspace.dock.hide();
cx.notify();
}
let pane_id = pane.id();
cx.subscribe(&pane, move |workspace, _, event, cx| {
workspace.handle_pane_event(pane_id, event, cx);
})
.detach();
Self {
pane,
position: Default::default(),
position: DockPosition::Hidden(cx.global::<Settings>().default_dock_anchor),
default_item_factory,
}
}
pub fn pane(&self) -> ViewHandle<Pane> {
self.pane.clone()
pub fn pane(&self) -> &ViewHandle<Pane> {
&self.pane
}
fn hide(&mut self) {
self.position = self.position.hide();
pub fn visible_pane(&self) -> Option<&ViewHandle<Pane>> {
self.position.visible().map(|_| self.pane())
}
fn ensure_not_empty(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
let pane = workspace.dock.pane.clone();
if pane.read(cx).items().next().is_none() {
let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
fn set_dock_position(
workspace: &mut Workspace,
new_position: DockPosition,
cx: &mut ViewContext<Workspace>,
) {
workspace.dock.position = new_position;
let now_visible = workspace.dock.visible_pane().is_some();
if now_visible {
// Ensure that the pane has at least one item or construct a default item to put in it
let pane = workspace.dock.pane.clone();
if pane.read(cx).items().next().is_none() {
let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
}
cx.focus(pane);
} else {
if let Some(last_active_center_pane) = workspace.last_active_center_pane.clone() {
cx.focus(last_active_center_pane);
}
}
cx.notify();
}
pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
}
fn toggle(workspace: &mut Workspace, _: &ToggleDock, cx: &mut ViewContext<Workspace>) {
// Shift-escape ON
// Get or insert the dock's last focused terminal
// Open the dock in fullscreen
// Focus that terminal
// Shift-escape OFF
// Close the dock
// Return focus to center
// Behaviors:
// If the dock is shown, hide it
// If the dock is hidden, show it
// If the dock was full screen, open it in last position (bottom or right)
// If the dock was bottom or right, re-open it in that context (and with the previous % width)
workspace.dock.position = workspace.dock.position.toggle();
if workspace.dock.position.visible().is_some() {
Self::ensure_not_empty(workspace, cx);
cx.focus(workspace.dock.pane.clone());
} else {
cx.focus_self();
}
cx.notify();
workspace.status_bar().update(cx, |_, cx| cx.notify());
Self::set_dock_position(workspace, workspace.dock.position.toggle(), cx);
}
fn move_dock(
@ -143,17 +128,54 @@ impl Dock {
&MoveDock(new_anchor): &MoveDock,
cx: &mut ViewContext<Workspace>,
) {
// Clear the previous position if the dock is not visible.
workspace.dock.position = DockPosition::Shown(new_anchor);
Self::ensure_not_empty(workspace, cx);
cx.notify();
Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), cx);
}
pub fn render(&self, _theme: &Theme, anchor: DockAnchor) -> Option<ElementBox> {
pub fn render(
&self,
theme: &Theme,
anchor: DockAnchor,
cx: &mut RenderContext<Workspace>,
) -> Option<ElementBox> {
let style = &theme.workspace.dock;
self.position
.visible()
.filter(|current_anchor| *current_anchor == anchor)
.map(|_| ChildView::new(self.pane.clone()).boxed())
.map(|anchor| match anchor {
DockAnchor::Bottom | DockAnchor::Right => {
let mut panel_style = style.panel.clone();
if anchor == DockAnchor::Bottom {
panel_style.margin = Margin {
top: panel_style.margin.top,
..Default::default()
};
} else {
panel_style.margin = Margin {
left: panel_style.margin.left,
..Default::default()
};
}
FlexItem::new(
Container::new(ChildView::new(self.pane.clone()).boxed())
.with_style(style.panel)
.boxed(),
)
.flex(style.flex, true)
.boxed()
}
DockAnchor::Expanded => Container::new(
MouseEventHandler::new::<Dock, _, _>(0, cx, |_state, _cx| {
Container::new(ChildView::new(self.pane.clone()).boxed())
.with_style(style.maximized)
.boxed()
})
.capture_all()
.with_cursor_style(CursorStyle::Arrow)
.boxed(),
)
.with_background_color(style.wash_color)
.boxed(),
})
}
}

View file

@ -1,8 +1,7 @@
use super::{ItemHandle, SplitDirection};
use crate::{
dock::{DockAnchor, MoveDock},
toolbar::Toolbar,
Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace,
dock::MoveDock, toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, WeakItemHandle,
Workspace,
};
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
@ -25,7 +24,7 @@ use gpui::{
};
use project::{Project, ProjectEntryId, ProjectPath};
use serde::Deserialize;
use settings::{Autosave, Settings};
use settings::{Autosave, DockAnchor, Settings};
use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
use theme::Theme;
use util::ResultExt;
@ -187,6 +186,7 @@ pub fn init(cx: &mut MutableAppContext) {
});
}
#[derive(Debug)]
pub enum Event {
Focused,
ActivateItem { local: bool },
@ -1392,7 +1392,7 @@ impl View for Pane {
.with_cursor_style(CursorStyle::PointingHand)
.on_down(MouseButton::Left, |e, cx| {
cx.dispatch_action(DeployNewMenu {
position: e.position,
position: e.region.lower_right(),
});
})
.boxed(),
@ -1422,11 +1422,11 @@ impl View for Pane {
.on_down(MouseButton::Left, move |e, cx| {
if is_dock {
cx.dispatch_action(DeployDockMenu {
position: e.position,
position: e.region.lower_right(),
});
} else {
cx.dispatch_action(DeploySplitMenu {
position: e.position,
position: e.region.lower_right(),
});
}
})
@ -1613,7 +1613,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
let (_, workspace) =
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
// 1. Add with a destination index
@ -1701,7 +1702,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
let (_, workspace) =
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
// 1. Add with a destination index
@ -1777,7 +1779,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
let (_, workspace) =
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
// singleton view

View file

@ -18,7 +18,7 @@ use client::{
};
use clock::ReplicaId;
use collections::{hash_map, HashMap, HashSet};
use dock::{DefaultItemFactory, Dock, DockAnchor, ToggleDockButton};
use dock::{DefaultItemFactory, Dock, ToggleDockButton};
use drag_and_drop::DragAndDrop;
use futures::{channel::oneshot, FutureExt};
use gpui::{
@ -41,7 +41,7 @@ use postage::prelude::Stream;
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
use searchable::SearchableItemHandle;
use serde::Deserialize;
use settings::{Autosave, Settings};
use settings::{Autosave, DockAnchor, Settings};
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
use smallvec::SmallVec;
use status_bar::StatusBar;
@ -892,6 +892,7 @@ pub struct Workspace {
panes: Vec<ViewHandle<Pane>>,
panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
active_pane: ViewHandle<Pane>,
last_active_center_pane: Option<ViewHandle<Pane>>,
status_bar: ViewHandle<StatusBar>,
dock: Dock,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
@ -987,6 +988,7 @@ impl Workspace {
cx.emit_global(WorkspaceCreated(weak_self.clone()));
let dock = Dock::new(cx, dock_default_factory);
let dock_pane = dock.pane().clone();
let left_sidebar = cx.add_view(|_| Sidebar::new(Side::Left));
let right_sidebar = cx.add_view(|_| Sidebar::new(Side::Right));
@ -1011,9 +1013,10 @@ impl Workspace {
weak_self,
center: PaneGroup::new(center_pane.clone()),
dock,
panes: vec![center_pane.clone()],
panes: vec![center_pane.clone(), dock_pane],
panes_by_item: Default::default(),
active_pane: center_pane.clone(),
last_active_center_pane: Some(center_pane.clone()),
status_bar,
notifications: Default::default(),
client,
@ -1556,10 +1559,6 @@ impl Workspace {
Pane::add_item(self, &active_pane, item, true, true, None, cx);
}
pub fn add_item_to_dock(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
Pane::add_item(self, &self.dock.pane(), item, true, true, None, cx);
}
pub fn open_path(
&mut self,
path: impl Into<ProjectPath>,
@ -1696,6 +1695,10 @@ impl Workspace {
status_bar.set_active_pane(&self.active_pane, cx);
});
self.active_item_path_changed(cx);
if &pane != self.dock.pane() {
self.last_active_center_pane = Some(pane.clone());
}
cx.notify();
}
@ -1715,21 +1718,19 @@ impl Workspace {
cx: &mut ViewContext<Self>,
) {
if let Some(pane) = self.pane(pane_id) {
let is_dock = &pane == self.dock.pane();
match event {
pane::Event::Split(direction) => {
pane::Event::Split(direction) if !is_dock => {
self.split_pane(pane, *direction, cx);
}
pane::Event::Remove => {
self.remove_pane(pane, cx);
}
pane::Event::Focused => {
self.handle_pane_focused(pane, cx);
}
pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
pane::Event::Remove if is_dock => Dock::hide(self, cx),
pane::Event::Focused => self.handle_pane_focused(pane, cx),
pane::Event::ActivateItem { local } => {
if *local {
self.unfollow(&pane, cx);
}
if pane == self.active_pane {
if &pane == self.active_pane() {
self.active_item_path_changed(cx);
}
}
@ -1747,8 +1748,9 @@ impl Workspace {
}
}
}
_ => {}
}
} else {
} else if self.dock.visible_pane().is_none() {
error!("pane {} not found", pane_id);
}
}
@ -1779,6 +1781,10 @@ impl Workspace {
for removed_item in pane.read(cx).items() {
self.panes_by_item.remove(&removed_item.id());
}
if self.last_active_center_pane == Some(pane) {
self.last_active_center_pane = None;
}
cx.notify();
} else {
self.active_item_path_changed(cx);
@ -1797,6 +1803,10 @@ impl Workspace {
&self.active_pane
}
pub fn dock_pane(&self) -> &ViewHandle<Pane> {
self.dock.pane()
}
fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
if let Some(remote_id) = remote_id {
self.remote_entity_subscription =
@ -2582,25 +2592,17 @@ impl View for Workspace {
.flex(1., true)
.boxed(),
)
.with_children(
self.dock
.render(&theme, DockAnchor::Bottom)
.map(|dock| {
FlexItem::new(dock)
.flex(1., true)
.boxed()
}),
)
.with_children(self.dock.render(
&theme,
DockAnchor::Bottom,
cx,
))
.boxed(),
)
.flex(1., true)
.boxed(),
)
.with_children(
self.dock
.render(&theme, DockAnchor::Right)
.map(|dock| FlexItem::new(dock).flex(1., true).boxed()),
)
.with_children(self.dock.render(&theme, DockAnchor::Right, cx))
.with_children(
if self.right_sidebar.read(cx).active_item().is_some() {
Some(
@ -2614,13 +2616,7 @@ impl View for Workspace {
)
.boxed()
})
.with_children(self.dock.render(&theme, DockAnchor::Expanded).map(
|dock| {
Container::new(dock)
.with_style(theme.workspace.fullscreen_dock)
.boxed()
},
))
.with_children(self.dock.render(&theme, DockAnchor::Expanded, cx))
.with_children(self.modal.as_ref().map(|m| {
ChildView::new(m)
.contained()

View file

@ -243,6 +243,7 @@ pub fn initialize_workspace(
.detach();
cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
let settings = cx.global::<Settings>();

View file

@ -156,9 +156,17 @@ export default function workspace(theme: Theme) {
width: 400,
margin: { right: 10, bottom: 10 },
},
fullscreenDock: {
background: withOpacity(theme.backgroundColor[500].base, 0.8),
padding: 25,
dock: {
wash_color: withOpacity(theme.backgroundColor[500].base, 0.5),
flex: 0.5,
panel: {
margin: 4,
},
maximized: {
margin: 32,
border: border(theme, "secondary"),
shadow: modalShadow(theme),
}
}
};
}