diff --git a/assets/settings/default.json b/assets/settings/default.json index 95ba08c9d5..127e0e1401 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -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: // diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d9d0c3990c..7e3b8ead9b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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, - style: EditorStyle, + style: Arc, 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, + position_map: &Arc, + visible_bounds: RectF, + text_bounds: RectF, + gutter_bounds: RectF, + bounds: RectF, + cx: &mut PaintContext, + ) { + enum EditorElementMouseHandlers {} + cx.scene.push_mouse_region( + MouseRegion::new::(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, 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, 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, gutter_size: Vector2F, gutter_padding: f32, gutter_margin: f32, text_size: Vector2F, - snapshot: EditorSnapshot, active_rows: BTreeMap, highlighted_rows: Option>, - line_layouts: Vec, line_number_layouts: Vec>, blocks: Vec, - line_height: f32, - em_width: f32, - em_advance: f32, highlighted_ranges: Vec<(Range, Color)>, selections: Vec<(ReplicaId, Vec)>, context_menu: Option<(DisplayPoint, ElementBox)>, @@ -1700,6 +1738,52 @@ pub struct LayoutState { hover_popovers: Option<(DisplayPoint, Vec)>, } +pub struct PositionMap { + size: Vector2F, + line_height: f32, + scroll_max: Vector2F, + em_width: f32, + em_advance: f32, + line_layouts: Vec, + 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, hover_popover_bounds: Vec, } -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 diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index e9091d74c8..19446ab0cc 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -4028,7 +4028,7 @@ pub struct RenderParams { pub view_id: usize, pub titlebar_height: f32, pub hovered_region_ids: HashSet, - pub clicked_region_ids: Option<(Vec, MouseButton)>, + pub clicked_region_ids: Option<(HashSet, MouseButton)>, pub refreshing: bool, } @@ -4037,7 +4037,7 @@ pub struct RenderContext<'a, T: View> { pub(crate) view_id: usize, pub(crate) view_type: PhantomData, pub(crate) hovered_region_ids: HashSet, - pub(crate) clicked_region_ids: Option<(Vec, MouseButton)>, + pub(crate) clicked_region_ids: Option<(HashSet, MouseButton)>, pub app: &'a mut MutableAppContext, pub titlebar_height: f32, pub refreshing: bool, diff --git a/crates/gpui/src/elements/event_handler.rs b/crates/gpui/src/elements/event_handler.rs index 015e778314..064f00e1f3 100644 --- a/crates/gpui/src/elements/event_handler.rs +++ b/crates/gpui/src/elements/event_handler.rs @@ -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); diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 9e5465e91a..25e2ba2fde 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -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); } diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index d81c939061..b402275919 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -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::(), cx.current_view_id()), + handlers: Default::default(), + hoverable: true, }); } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 859ba46375..91cd58f707 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -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, last_mouse_moved_event: Option, hovered_region_ids: HashSet, - clicked_regions: Vec, + clicked_region_ids: HashSet, clicked_button: Option, 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(®ion_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(®ion.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(®ion_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, - clicked_region_ids: Option<(Vec, MouseButton)>, + clicked_region_ids: Option<(HashSet, MouseButton)>, } impl<'a> LayoutContext<'a> { diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 086af5f64d..a00f354d3d 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -536,11 +536,11 @@ impl ToJson for Border { } impl MouseRegion { - pub fn id(&self) -> Option { - self.discriminant.map(|discriminant| MouseRegionId { + pub fn id(&self) -> MouseRegionId { + MouseRegionId { view_id: self.view_id, - discriminant, - }) + discriminant: self.discriminant, + } } } diff --git a/crates/gpui/src/scene/mouse_region.rs b/crates/gpui/src/scene/mouse_region.rs index 362818134e..e9c0ed31ca 100644 --- a/crates/gpui/src/scene/mouse_region.rs +++ b/crates/gpui/src/scene/mouse_region.rs @@ -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(view_id: usize, region_id: usize, bounds: RectF) -> Self { + Self::from_handlers::(view_id, region_id, bounds, Default::default()) } - pub fn from_handlers( + pub fn handle_all(view_id: usize, region_id: usize, bounds: RectF) -> Self { + Self::from_handlers::(view_id, region_id, bounds, HandlerSet::capture_all()) + } + + pub fn from_handlers( view_id: usize, - discriminant: Option<(TypeId, usize)>, + region_id: usize, bounds: RectF, handlers: HandlerSet, ) -> Self { Self { view_id, - discriminant, + discriminant: (TypeId::of::(), 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 + } } diff --git a/crates/gpui/src/scene/mouse_region_event.rs b/crates/gpui/src/scene/mouse_region_event.rs index 4aff9f1ab9..4d89cd5b6f 100644 --- a/crates/gpui/src/scene/mouse_region_event.rs +++ b/crates/gpui/src/scene/mouse_region_event.rs @@ -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, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 1095f289eb..fa5fa9d2a9 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -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, @@ -167,6 +177,8 @@ pub struct SettingsFileContent { pub vim_mode: Option, #[serde(default)] pub autosave: Option, + #[serde(default)] + pub default_dock_anchor: Option, #[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), diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index d88511cbce..fb2fb0211a 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -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::(view_id, view_id, visible_bounds); // Terminal Emulator controlled behavior: region = region diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index b4ddd8ed94..3bf643ec24 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -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, } +#[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)] diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 7bc407e508..c928d72c76 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -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, 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::().default_dock_anchor), default_item_factory, } } - pub fn pane(&self) -> ViewHandle { - self.pane.clone() + pub fn pane(&self) -> &ViewHandle { + &self.pane } - fn hide(&mut self) { - self.position = self.position.hide(); + pub fn visible_pane(&self) -> Option<&ViewHandle> { + self.position.visible().map(|_| self.pane()) } - fn ensure_not_empty(workspace: &mut Workspace, cx: &mut ViewContext) { - 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.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) { + Self::set_dock_position(workspace, workspace.dock.position.hide(), cx); } fn toggle(workspace: &mut Workspace, _: &ToggleDock, cx: &mut ViewContext) { - // 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, ) { - // 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 { + pub fn render( + &self, + theme: &Theme, + anchor: DockAnchor, + cx: &mut RenderContext, + ) -> Option { + 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::(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(), + }) } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index f8e95dcb33..5f4a7a1962 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -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 diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a67910d694..1b5cd413b7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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>, panes_by_item: HashMap>, active_pane: ViewHandle, + last_active_center_pane: Option>, status_bar: ViewHandle, dock: Dock, notifications: Vec<(TypeId, usize, Box)>, @@ -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, cx: &mut ViewContext) { - Pane::add_item(self, &self.dock.pane(), item, true, true, None, cx); - } - pub fn open_path( &mut self, path: impl Into, @@ -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, ) { 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 { + self.dock.pane() + } + fn project_remote_id_changed(&mut self, remote_id: Option, cx: &mut ViewContext) { 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() diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 103db1b515..73e87b0fce 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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::(); diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index d47b76cd05..b775578e3a 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -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), + } } }; }