diff --git a/Cargo.lock b/Cargo.lock index 3b45a329183efcbf686be97aaefb7dbf29990fb0..1747eae2d25fb958fa096a748793a3b0f24ab02b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8823,6 +8823,17 @@ dependencies = [ "util", ] +[[package]] +name = "storybook3" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui2", + "settings2", + "theme2", + "ui2", +] + [[package]] name = "stringprep" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index f8d0af77fa85220f348c866edfe5242d0cccbeec..f107dc5390af2ec57b62e2f3b6cf3ac16b9316c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,6 +95,7 @@ members = [ "crates/sqlez_macros", "crates/rich_text", "crates/storybook2", + "crates/storybook3", "crates/sum_tree", "crates/terminal", "crates/terminal2", diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 6264606ed9c7db0116d475ac48a57eef872434a5..9463cab68ca1b76984f372573e1271cb6fac76fc 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,9 +1,8 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle, - Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView, - WindowContext, + actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke, + ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use std::{ @@ -16,7 +15,7 @@ use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, }; -use workspace::{Modal, ModalEvent, Workspace}; +use workspace::Workspace; use zed_actions::OpenZedURL; actions!(Toggle); @@ -69,10 +68,9 @@ impl CommandPalette { } } -impl EventEmitter for CommandPalette {} -impl Modal for CommandPalette { - fn focus(&self, cx: &mut WindowContext) { - self.picker.update(cx, |picker, cx| picker.focus(cx)); +impl ManagedView for CommandPalette { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) } } @@ -267,7 +265,7 @@ impl PickerDelegate for CommandPaletteDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.command_palette - .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .update(cx, |_, cx| cx.emit(Dismiss)) .log_err(); } diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index e64d5e301caa0912d6309280e083ed3006225f19..533abcd871b6165bc3f400dbceda69122b71b361 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -13,7 +13,8 @@ pub use block_map::{BlockMap, BlockPoint}; use collections::{BTreeMap, HashMap, HashSet}; use fold_map::FoldMap; use gpui::{ - Font, FontId, HighlightStyle, Hsla, Line, Model, ModelContext, Pixels, TextRun, UnderlineStyle, + Font, FontId, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, ShapedLine, + TextRun, UnderlineStyle, WrappedLine, }; use inlay_map::InlayMap; use language::{ @@ -561,7 +562,7 @@ impl DisplaySnapshot { }) } - pub fn lay_out_line_for_row( + pub fn layout_row( &self, display_row: u32, TextLayoutDetails { @@ -569,7 +570,7 @@ impl DisplaySnapshot { editor_style, rem_size, }: &TextLayoutDetails, - ) -> Line { + ) -> Arc { let mut runs = Vec::new(); let mut line = String::new(); @@ -598,29 +599,27 @@ impl DisplaySnapshot { let font_size = editor_style.text.font_size.to_pixels(*rem_size); text_system - .layout_text(&line, font_size, &runs, None) - .unwrap() - .pop() - .unwrap() + .layout_line(&line, font_size, &runs) + .expect("we expect the font to be loaded because it's rendered by the editor") } - pub fn x_for_point( + pub fn x_for_display_point( &self, display_point: DisplayPoint, text_layout_details: &TextLayoutDetails, ) -> Pixels { - let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details); - layout_line.x_for_index(display_point.column() as usize) + let line = self.layout_row(display_point.row(), text_layout_details); + line.x_for_index(display_point.column() as usize) } - pub fn column_for_x( + pub fn display_column_for_x( &self, display_row: u32, - x_coordinate: Pixels, - text_layout_details: &TextLayoutDetails, + x: Pixels, + details: &TextLayoutDetails, ) -> u32 { - let layout_line = self.lay_out_line_for_row(display_row, text_layout_details); - layout_line.closest_index_for_x(x_coordinate) as u32 + let layout_line = self.layout_row(display_row, details); + layout_line.closest_index_for_x(x) as u32 } pub fn chars_at( diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 4e5c8ed2850365a329443488484d88002514568b..b1dc76852dd0fc320ba39c9d7fcad35370f225e9 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -5445,7 +5445,9 @@ impl Editor { *head.column_mut() += 1; head = display_map.clip_point(head, Bias::Right); let goal = SelectionGoal::HorizontalPosition( - display_map.x_for_point(head, &text_layout_details).into(), + display_map + .x_for_display_point(head, &text_layout_details) + .into(), ); selection.collapse_to(head, goal); @@ -6391,8 +6393,8 @@ impl Editor { let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); let range = oldest_selection.display_range(&display_map).sorted(); - let start_x = display_map.x_for_point(range.start, &text_layout_details); - let end_x = display_map.x_for_point(range.end, &text_layout_details); + let start_x = display_map.x_for_display_point(range.start, &text_layout_details); + let end_x = display_map.x_for_display_point(range.end, &text_layout_details); let positions = start_x.min(end_x)..start_x.max(end_x); selections.clear(); @@ -6431,15 +6433,16 @@ impl Editor { let range = selection.display_range(&display_map).sorted(); debug_assert_eq!(range.start.row(), range.end.row()); let mut row = range.start.row(); - let positions = if let SelectionGoal::HorizontalRange { start, end } = - selection.goal - { - px(start)..px(end) - } else { - let start_x = display_map.x_for_point(range.start, &text_layout_details); - let end_x = display_map.x_for_point(range.end, &text_layout_details); - start_x.min(end_x)..start_x.max(end_x) - }; + let positions = + if let SelectionGoal::HorizontalRange { start, end } = selection.goal { + px(start)..px(end) + } else { + let start_x = + display_map.x_for_display_point(range.start, &text_layout_details); + let end_x = + display_map.x_for_display_point(range.end, &text_layout_details); + start_x.min(end_x)..start_x.max(end_x) + }; while row != end_row { if above { @@ -6992,7 +6995,7 @@ impl Editor { let display_point = point.to_display_point(display_snapshot); let goal = SelectionGoal::HorizontalPosition( display_snapshot - .x_for_point(display_point, &text_layout_details) + .x_for_display_point(display_point, &text_layout_details) .into(), ); (display_point, goal) @@ -9759,7 +9762,8 @@ impl InputHandler for Editor { let scroll_left = scroll_position.x * em_width; let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); - let x = snapshot.x_for_point(start, &text_layout_details) - scroll_left + self.gutter_width; + let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left + + self.gutter_width; let y = line_height * (start.row() as f32 - scroll_position.y); Some(Bounds { diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index aa54bed73a9fbb5883fc30651b752c02240f4eb4..3de5389b1f8f83f8046fb4cef090de5994ea63d3 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -20,10 +20,10 @@ use collections::{BTreeMap, HashMap}; use gpui::{ div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, - ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, Line, + ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, - ScrollWheelEvent, Size, StatefulInteractiveComponent, Style, Styled, TextRun, TextStyle, View, - ViewContext, WindowContext, + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled, + TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -476,7 +476,7 @@ impl EditorElement { Self::paint_diff_hunks(bounds, layout, cx); } - for (ix, line) in layout.line_number_layouts.iter().enumerate() { + for (ix, line) in layout.line_numbers.iter().enumerate() { if let Some(line) = line { let line_origin = bounds.origin + point( @@ -775,21 +775,21 @@ impl EditorElement { .chars_at(cursor_position) .next() .and_then(|(character, _)| { - let text = character.to_string(); + let text = SharedString::from(character.to_string()); + let len = text.len(); cx.text_system() - .layout_text( - &text, + .shape_line( + text, cursor_row_layout.font_size, &[TextRun { - len: text.len(), + len, font: self.style.text.font(), color: self.style.background, + background_color: None, underline: None, }], - None, ) - .unwrap() - .pop() + .log_err() }) } else { None @@ -1244,20 +1244,20 @@ impl EditorElement { let font_size = style.text.font_size.to_pixels(cx.rem_size()); let layout = cx .text_system() - .layout_text( - " ".repeat(column).as_str(), + .shape_line( + SharedString::from(" ".repeat(column)), font_size, &[TextRun { len: column, font: style.text.font(), color: Hsla::default(), + background_color: None, underline: None, }], - None, ) .unwrap(); - layout[0].width + layout.width } fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext) -> Pixels { @@ -1338,7 +1338,7 @@ impl EditorElement { relative_rows } - fn layout_line_numbers( + fn shape_line_numbers( &self, rows: Range, active_rows: &BTreeMap, @@ -1347,12 +1347,12 @@ impl EditorElement { snapshot: &EditorSnapshot, cx: &ViewContext, ) -> ( - Vec>, + Vec>, Vec>, ) { let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); let include_line_numbers = snapshot.mode == EditorMode::Full; - let mut line_number_layouts = Vec::with_capacity(rows.len()); + let mut shaped_line_numbers = Vec::with_capacity(rows.len()); let mut fold_statuses = Vec::with_capacity(rows.len()); let mut line_number = String::new(); let is_relative = EditorSettings::get_global(cx).relative_line_numbers; @@ -1387,15 +1387,14 @@ impl EditorElement { len: line_number.len(), font: self.style.text.font(), color, + background_color: None, underline: None, }; - let layout = cx + let shaped_line = cx .text_system() - .layout_text(&line_number, font_size, &[run], None) - .unwrap() - .pop() + .shape_line(line_number.clone().into(), font_size, &[run]) .unwrap(); - line_number_layouts.push(Some(layout)); + shaped_line_numbers.push(Some(shaped_line)); fold_statuses.push( is_singleton .then(|| { @@ -1408,17 +1407,17 @@ impl EditorElement { } } else { fold_statuses.push(None); - line_number_layouts.push(None); + shaped_line_numbers.push(None); } } - (line_number_layouts, fold_statuses) + (shaped_line_numbers, fold_statuses) } fn layout_lines( &mut self, rows: Range, - line_number_layouts: &[Option], + line_number_layouts: &[Option], snapshot: &EditorSnapshot, cx: &ViewContext, ) -> Vec { @@ -1439,18 +1438,17 @@ impl EditorElement { .chain(iter::repeat("")) .take(rows.len()); placeholder_lines - .map(|line| { + .filter_map(move |line| { let run = TextRun { len: line.len(), font: self.style.text.font(), color: placeholder_color, + background_color: None, underline: Default::default(), }; cx.text_system() - .layout_text(line, font_size, &[run], None) - .unwrap() - .pop() - .unwrap() + .shape_line(line.to_string().into(), font_size, &[run]) + .log_err() }) .map(|line| LineWithInvisibles { line, @@ -1726,7 +1724,7 @@ impl EditorElement { .head }); - let (line_number_layouts, fold_statuses) = self.layout_line_numbers( + let (line_numbers, fold_statuses) = self.shape_line_numbers( start_row..end_row, &active_rows, head_for_relative, @@ -1740,8 +1738,7 @@ impl EditorElement { let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines); let mut max_visible_line_width = Pixels::ZERO; - let line_layouts = - self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx); + let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx); for line_with_invisibles in &line_layouts { if line_with_invisibles.line.width > max_visible_line_width { max_visible_line_width = line_with_invisibles.line.width; @@ -1879,35 +1876,31 @@ impl EditorElement { let invisible_symbol_font_size = font_size / 2.; let tab_invisible = cx .text_system() - .layout_text( - "→", + .shape_line( + "→".into(), invisible_symbol_font_size, &[TextRun { len: "→".len(), font: self.style.text.font(), color: cx.theme().colors().editor_invisible, + background_color: None, underline: None, }], - None, ) - .unwrap() - .pop() .unwrap(); let space_invisible = cx .text_system() - .layout_text( - "•", + .shape_line( + "•".into(), invisible_symbol_font_size, &[TextRun { len: "•".len(), font: self.style.text.font(), color: cx.theme().colors().editor_invisible, + background_color: None, underline: None, }], - None, ) - .unwrap() - .pop() .unwrap(); LayoutState { @@ -1939,7 +1932,7 @@ impl EditorElement { active_rows, highlighted_rows, highlighted_ranges, - line_number_layouts, + line_numbers, display_hunks, blocks, selections, @@ -2201,7 +2194,7 @@ impl EditorElement { #[derive(Debug)] pub struct LineWithInvisibles { - pub line: Line, + pub line: ShapedLine, invisibles: Vec, } @@ -2211,7 +2204,7 @@ impl LineWithInvisibles { text_style: &TextStyle, max_line_len: usize, max_line_count: usize, - line_number_layouts: &[Option], + line_number_layouts: &[Option], editor_mode: EditorMode, cx: &WindowContext, ) -> Vec { @@ -2231,11 +2224,12 @@ impl LineWithInvisibles { }]) { for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() { if ix > 0 { - let layout = cx + let shaped_line = cx .text_system() - .layout_text(&line, font_size, &styles, None); + .shape_line(line.clone().into(), font_size, &styles) + .unwrap(); layouts.push(Self { - line: layout.unwrap().pop().unwrap(), + line: shaped_line, invisibles: invisibles.drain(..).collect(), }); @@ -2269,6 +2263,7 @@ impl LineWithInvisibles { len: line_chunk.len(), font: text_style.font(), color: text_style.color, + background_color: None, underline: text_style.underline, }); @@ -3089,7 +3084,7 @@ pub struct LayoutState { visible_display_row_range: Range, active_rows: BTreeMap, highlighted_rows: Option>, - line_number_layouts: Vec>, + line_numbers: Vec>, display_hunks: Vec, blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, @@ -3102,8 +3097,8 @@ pub struct LayoutState { code_actions_indicator: Option, // hover_popovers: Option<(DisplayPoint, Vec>)>, fold_indicators: Vec>>, - tab_invisible: Line, - space_invisible: Line, + tab_invisible: ShapedLine, + space_invisible: ShapedLine, } struct CodeActionsIndicator { @@ -3203,7 +3198,7 @@ fn layout_line( snapshot: &EditorSnapshot, style: &EditorStyle, cx: &WindowContext, -) -> Result { +) -> Result { let mut line = snapshot.line(row); if line.len() > MAX_LINE_LEN { @@ -3215,21 +3210,17 @@ fn layout_line( line.truncate(len); } - Ok(cx - .text_system() - .layout_text( - &line, - style.text.font_size.to_pixels(cx.rem_size()), - &[TextRun { - len: snapshot.line_len(row) as usize, - font: style.text.font(), - color: Hsla::default(), - underline: None, - }], - None, - )? - .pop() - .unwrap()) + cx.text_system().shape_line( + line.into(), + style.text.font_size.to_pixels(cx.rem_size()), + &[TextRun { + len: snapshot.line_len(row) as usize, + font: style.text.font(), + color: Hsla::default(), + background_color: None, + underline: None, + }], + ) } #[derive(Debug)] @@ -3239,7 +3230,7 @@ pub struct Cursor { line_height: Pixels, color: Hsla, shape: CursorShape, - block_text: Option, + block_text: Option, } impl Cursor { @@ -3249,7 +3240,7 @@ impl Cursor { line_height: Pixels, color: Hsla, shape: CursorShape, - block_text: Option, + block_text: Option, ) -> Cursor { Cursor { origin, diff --git a/crates/editor2/src/movement.rs b/crates/editor2/src/movement.rs index b28af681e0f2eaf2f5f838be838ff696f1ab3b9b..1414ae702dc4f772e3fcd895345eb849ccbc9217 100644 --- a/crates/editor2/src/movement.rs +++ b/crates/editor2/src/movement.rs @@ -98,7 +98,7 @@ pub fn up_by_rows( SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.") SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), SelectionGoal::HorizontalRange { end, .. } => end.into(), - _ => map.x_for_point(start, text_layout_details), + _ => map.x_for_display_point(start, text_layout_details), }; let prev_row = start.row().saturating_sub(row_count); @@ -107,7 +107,7 @@ pub fn up_by_rows( Bias::Left, ); if point.row() < start.row() { - *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details) + *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details) } else if preserve_column_at_start { return (start, goal); } else { @@ -137,18 +137,18 @@ pub fn down_by_rows( SelectionGoal::HorizontalPosition(x) => x.into(), SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), SelectionGoal::HorizontalRange { end, .. } => end.into(), - _ => map.x_for_point(start, text_layout_details), + _ => map.x_for_display_point(start, text_layout_details), }; let new_row = start.row() + row_count; let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right); if point.row() > start.row() { - *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details) + *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details) } else if preserve_column_at_end { return (start, goal); } else { point = map.max_point(); - goal_x = map.x_for_point(point, text_layout_details) + goal_x = map.x_for_display_point(point, text_layout_details) } let mut clipped_point = map.clip_point(point, Bias::Right); diff --git a/crates/editor2/src/selections_collection.rs b/crates/editor2/src/selections_collection.rs index 01e241c830d2d71edf74eb4a7cabfca0e175f12d..bcf41f135ba44b0562d1351901a452b04a1ef10b 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/crates/editor2/src/selections_collection.rs @@ -313,14 +313,14 @@ impl SelectionsCollection { let is_empty = positions.start == positions.end; let line_len = display_map.line_len(row); - let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details); + let line = display_map.layout_row(row, &text_layout_details); dbg!("****START COL****"); - let start_col = layed_out_line.closest_index_for_x(positions.start) as u32; - if start_col < line_len || (is_empty && positions.start == layed_out_line.width) { + let start_col = line.closest_index_for_x(positions.start) as u32; + if start_col < line_len || (is_empty && positions.start == line.width) { let start = DisplayPoint::new(row, start_col); dbg!("****END COL****"); - let end_col = layed_out_line.closest_index_for_x(positions.end) as u32; + let end_col = line.closest_index_for_x(positions.end) as u32; let end = DisplayPoint::new(row, end_col); dbg!(start_col, end_col); diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index b2850761a97a373f9fcdb6b01ea0c5c9acd80b22..0fee5102e6d0314d134848eb3abf5697d71003d5 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -2,9 +2,9 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, div, AppContext, Component, Div, EventEmitter, InteractiveComponent, Model, - ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, - WindowContext, + actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveComponent, + ManagedView, Model, ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, + WeakView, }; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; @@ -19,7 +19,7 @@ use text::Point; use theme::ActiveTheme; use ui::{v_stack, HighlightedLabel, StyledExt}; use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; -use workspace::{Modal, ModalEvent, Workspace}; +use workspace::Workspace; actions!(Toggle); @@ -111,10 +111,9 @@ impl FileFinder { } } -impl EventEmitter for FileFinder {} -impl Modal for FileFinder { - fn focus(&self, cx: &mut WindowContext) { - self.picker.update(cx, |picker, cx| picker.focus(cx)) +impl ManagedView for FileFinder { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) } } impl Render for FileFinder { @@ -689,9 +688,7 @@ impl PickerDelegate for FileFinderDelegate { .log_err(); } } - finder - .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed)) - .ok()?; + finder.update(&mut cx, |_, cx| cx.emit(Dismiss)).ok()?; Some(()) }) @@ -702,7 +699,7 @@ impl PickerDelegate for FileFinderDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.file_finder - .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .update(cx, |_, cx| cx.emit(Dismiss)) .log_err(); } diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index ccd6b7ada2141ef0cf0d02ba3bbae34cc6a926f2..565afb5e939f01225341ae84e1628ead5daf5cbd 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,13 +1,13 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString, - Styled, Subscription, View, ViewContext, VisualContext, WindowContext, + actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent, + Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; use text::{Bias, Point}; use theme::ActiveTheme; use ui::{h_stack, v_stack, Label, StyledExt, TextColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; -use workspace::{Modal, ModalEvent, Workspace}; +use workspace::Workspace; actions!(Toggle); @@ -23,10 +23,9 @@ pub struct GoToLine { _subscriptions: Vec, } -impl EventEmitter for GoToLine {} -impl Modal for GoToLine { - fn focus(&self, cx: &mut WindowContext) { - self.line_editor.update(cx, |editor, cx| editor.focus(cx)) +impl ManagedView for GoToLine { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.line_editor.focus_handle(cx) } } @@ -88,7 +87,7 @@ impl GoToLine { ) { match event { // todo!() this isn't working... - editor::Event::Blurred => cx.emit(ModalEvent::Dismissed), + editor::Event::Blurred => cx.emit(Dismiss), editor::Event::BufferEdited { .. } => self.highlight_current_line(cx), _ => {} } @@ -123,7 +122,7 @@ impl GoToLine { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(ModalEvent::Dismissed); + cx.emit(Dismiss); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -140,7 +139,7 @@ impl GoToLine { self.prev_scroll_position.take(); } - cx.emit(ModalEvent::Dismissed); + cx.emit(Dismiss); } } diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 221eb903fd4f7ef93df32558bbafef51d0518426..b4b1af630e981a2170c9fa6b1cdd5a6857ff2349 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -13,7 +13,7 @@ pub trait Element { fn layout( &mut self, view_state: &mut V, - previous_element_state: Option, + element_state: Option, cx: &mut ViewContext, ) -> (LayoutId, Self::ElementState); diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index ebbc34a48a08d7cab7f2978c8d5bdf730f569e68..f9560f2c53188a17b6c336e9bb284ee3ec58b07f 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -960,11 +960,11 @@ where cx.background_executor().timer(TOOLTIP_DELAY).await; view.update(&mut cx, move |view_state, cx| { active_tooltip.borrow_mut().replace(ActiveTooltip { - waiting: None, tooltip: Some(AnyTooltip { view: tooltip_builder(view_state, cx), cursor_offset: cx.mouse_position(), }), + _task: None, }); cx.notify(); }) @@ -972,12 +972,17 @@ where } }); active_tooltip.borrow_mut().replace(ActiveTooltip { - waiting: Some(task), tooltip: None, + _task: Some(task), }); } }); + let active_tooltip = element_state.active_tooltip.clone(); + cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| { + active_tooltip.borrow_mut().take(); + }); + if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() { if active_tooltip.tooltip.is_some() { cx.active_tooltip = active_tooltip.tooltip.clone() @@ -1207,9 +1212,8 @@ pub struct InteractiveElementState { } pub struct ActiveTooltip { - #[allow(unused)] // used to drop the task - waiting: Option>, tooltip: Option, + _task: Option>, } /// Whether or not the element or a group that contains it is clicked by the mouse. diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index a190337f04dbfe5a4acd908aaf4a646c33a49240..14a8048d398176bbd8bec49b37bc96b261450ed9 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -1,8 +1,9 @@ use smallvec::SmallVec; +use taffy::style::{Display, Position}; use crate::{ - point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentComponent, Pixels, Point, - Size, Style, + point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels, + Point, Size, Style, }; pub struct OverlayState { @@ -14,7 +15,7 @@ pub struct Overlay { anchor_corner: AnchorCorner, fit_mode: OverlayFitMode, // todo!(); - // anchor_position: Option, + anchor_position: Option>, // position_mode: OverlayPositionMode, } @@ -25,6 +26,7 @@ pub fn overlay() -> Overlay { children: SmallVec::new(), anchor_corner: AnchorCorner::TopLeft, fit_mode: OverlayFitMode::SwitchAnchor, + anchor_position: None, } } @@ -35,6 +37,13 @@ impl Overlay { self } + /// Sets the position in window co-ordinates + /// (otherwise the location the overlay is rendered is used) + pub fn position(mut self, anchor: Point) -> Self { + self.anchor_position = Some(anchor); + self + } + /// Snap to window edge instead of switching anchor corner when an overflow would occur. pub fn snap_to_window(mut self) -> Self { self.fit_mode = OverlayFitMode::SnapToWindow; @@ -48,6 +57,12 @@ impl ParentComponent for Overlay { } } +impl Component for Overlay { + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + impl Element for Overlay { type ElementState = OverlayState; @@ -66,7 +81,12 @@ impl Element for Overlay { .iter_mut() .map(|child| child.layout(view_state, cx)) .collect::>(); - let layout_id = cx.request_layout(&Style::default(), child_layout_ids.iter().copied()); + + let mut overlay_style = Style::default(); + overlay_style.position = Position::Absolute; + overlay_style.display = Display::Flex; + + let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied()); (layout_id, OverlayState { child_layout_ids }) } @@ -90,7 +110,7 @@ impl Element for Overlay { child_max = child_max.max(&child_bounds.lower_right()); } let size: Size = (child_max - child_min).into(); - let origin = bounds.origin; + let origin = self.anchor_position.unwrap_or(bounds.origin); let mut desired = self.anchor_corner.get_bounds(origin, size); let limits = Bounds { @@ -184,6 +204,15 @@ impl AnchorCorner { Bounds { origin, size } } + pub fn corner(&self, bounds: Bounds) -> Point { + match self { + Self::TopLeft => bounds.origin, + Self::TopRight => bounds.upper_right(), + Self::BottomLeft => bounds.lower_left(), + Self::BottomRight => bounds.lower_right(), + } + } + fn switch_axis(self, axis: Axis) -> Self { match axis { Axis::Vertical => match self { diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 1081154e7dfd90a08955ad63487b6e045791b57f..6849a8971107f011ee3e5ee06b186ada3da78ed4 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -1,76 +1,39 @@ use crate::{ - AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString, - Size, TextRun, ViewContext, + AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels, + SharedString, Size, TextRun, ViewContext, WrappedLine, }; -use parking_lot::Mutex; +use parking_lot::{Mutex, MutexGuard}; use smallvec::SmallVec; -use std::{marker::PhantomData, sync::Arc}; +use std::{cell::Cell, rc::Rc, sync::Arc}; use util::ResultExt; -impl Component for SharedString { - fn render(self) -> AnyElement { - Text { - text: self, - runs: None, - state_type: PhantomData, - } - .render() - } -} - -impl Component for &'static str { - fn render(self) -> AnyElement { - Text { - text: self.into(), - runs: None, - state_type: PhantomData, - } - .render() - } -} - -// TODO: Figure out how to pass `String` to `child` without this. -// This impl doesn't exist in the `gpui2` crate. -impl Component for String { - fn render(self) -> AnyElement { - Text { - text: self.into(), - runs: None, - state_type: PhantomData, - } - .render() - } -} - -pub struct Text { +pub struct Text { text: SharedString, runs: Option>, - state_type: PhantomData, } -impl Text { - /// styled renders text that has different runs of different styles. - /// callers are responsible for setting the correct style for each run. - //// - /// For uniform text you can usually just pass a string as a child, and - /// cx.text_style() will be used automatically. +impl Text { + /// Renders text with runs of different styles. + /// + /// Callers are responsible for setting the correct style for each run. + /// For text with a uniform style, you can usually avoid calling this constructor + /// and just pass text directly. pub fn styled(text: SharedString, runs: Vec) -> Self { Text { text, runs: Some(runs), - state_type: Default::default(), } } } -impl Component for Text { +impl Component for Text { fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Text { - type ElementState = Arc>>; +impl Element for Text { + type ElementState = TextState; fn element_id(&self) -> Option { None @@ -103,7 +66,7 @@ impl Element for Text { let element_state = element_state.clone(); move |known_dimensions, _| { let Some(lines) = text_system - .layout_text( + .shape_text( &text, font_size, &runs[..], @@ -111,30 +74,23 @@ impl Element for Text { ) .log_err() else { - element_state.lock().replace(TextElementState { + element_state.lock().replace(TextStateInner { lines: Default::default(), line_height, }); return Size::default(); }; - let line_count = lines - .iter() - .map(|line| line.wrap_count() + 1) - .sum::(); - let size = Size { - width: lines - .iter() - .map(|line| line.layout.width) - .max() - .unwrap() - .ceil(), - height: line_height * line_count, - }; + let mut size: Size = Size::default(); + for line in &lines { + let line_size = line.size(line_height); + size.height += line_size.height; + size.width = size.width.max(line_size.width); + } element_state .lock() - .replace(TextElementState { lines, line_height }); + .replace(TextStateInner { lines, line_height }); size } @@ -165,7 +121,104 @@ impl Element for Text { } } -pub struct TextElementState { - lines: SmallVec<[Line; 1]>, +#[derive(Default, Clone)] +pub struct TextState(Arc>>); + +impl TextState { + fn lock(&self) -> MutexGuard> { + self.0.lock() + } +} + +struct TextStateInner { + lines: SmallVec<[WrappedLine; 1]>, line_height: Pixels, } + +struct InteractiveText { + id: ElementId, + text: Text, +} + +struct InteractiveTextState { + text_state: TextState, + clicked_range_ixs: Rc>>, +} + +impl Element for InteractiveText { + type ElementState = InteractiveTextState; + + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut ViewContext, + ) -> (LayoutId, Self::ElementState) { + if let Some(InteractiveTextState { + text_state, + clicked_range_ixs, + }) = element_state + { + let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx); + let element_state = InteractiveTextState { + text_state, + clicked_range_ixs, + }; + (layout_id, element_state) + } else { + let (layout_id, text_state) = self.text.layout(view_state, None, cx); + let element_state = InteractiveTextState { + text_state, + clicked_range_ixs: Rc::default(), + }; + (layout_id, element_state) + } + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + self.text + .paint(bounds, view_state, &mut element_state.text_state, cx) + } +} + +impl Component for SharedString { + fn render(self) -> AnyElement { + Text { + text: self, + runs: None, + } + .render() + } +} + +impl Component for &'static str { + fn render(self) -> AnyElement { + Text { + text: self.into(), + runs: None, + } + .render() + } +} + +// TODO: Figure out how to pass `String` to `child` without this. +// This impl doesn't exist in the `gpui2` crate. +impl Component for String { + fn render(self) -> AnyElement { + Text { + text: self.into(), + runs: None, + } + .render() + } +} diff --git a/crates/gpui2/src/platform/mac/text_system.rs b/crates/gpui2/src/platform/mac/text_system.rs index 155f3097fec2e13b70355b78d9625fd74248bf82..9ef0f321b68933454f4f72d7aecbb04460669709 100644 --- a/crates/gpui2/src/platform/mac/text_system.rs +++ b/crates/gpui2/src/platform/mac/text_system.rs @@ -343,10 +343,10 @@ impl MacTextSystemState { // Construct the attributed string, converting UTF8 ranges to UTF16 ranges. let mut string = CFMutableAttributedString::new(); { - string.replace_str(&CFString::new(text), CFRange::init(0, 0)); + string.replace_str(&CFString::new(text.as_ref()), CFRange::init(0, 0)); let utf16_line_len = string.char_len() as usize; - let mut ix_converter = StringIndexConverter::new(text); + let mut ix_converter = StringIndexConverter::new(text.as_ref()); for run in font_runs { let utf8_end = ix_converter.utf8_ix + run.len; let utf16_start = ix_converter.utf16_ix; @@ -390,7 +390,7 @@ impl MacTextSystemState { }; let font_id = self.id_for_native_font(font); - let mut ix_converter = StringIndexConverter::new(text); + let mut ix_converter = StringIndexConverter::new(text.as_ref()); let mut glyphs = SmallVec::new(); for ((glyph_id, position), glyph_utf16_ix) in run .glyphs() @@ -413,11 +413,11 @@ impl MacTextSystemState { let typographic_bounds = line.get_typographic_bounds(); LineLayout { + runs, + font_size, width: typographic_bounds.width.into(), ascent: typographic_bounds.ascent.into(), descent: typographic_bounds.descent.into(), - runs, - font_size, len: text.len(), } } diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index d07df3d94b352bced85f5dfaa853ff788faf5876..03782d13a84a0cb36e681a6a06470054c61e28e5 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -1141,7 +1141,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) }; if let Some(mut event) = event { - let synthesized_second_event = match &mut event { + match &mut event { InputEvent::MouseDown( event @ MouseDownEvent { button: MouseButton::Left, @@ -1149,6 +1149,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { .. }, ) => { + // On mac, a ctrl-left click should be handled as a right click. *event = MouseDownEvent { button: MouseButton::Right, modifiers: Modifiers { @@ -1158,26 +1159,30 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { click_count: 1, ..*event }; - - Some(InputEvent::MouseDown(MouseDownEvent { - button: MouseButton::Right, - ..*event - })) } // Because we map a ctrl-left_down to a right_down -> right_up let's ignore // the ctrl-left_up to avoid having a mismatch in button down/up events if the // user is still holding ctrl when releasing the left mouse button - InputEvent::MouseUp(MouseUpEvent { - button: MouseButton::Left, - modifiers: Modifiers { control: true, .. }, - .. - }) => { - lock.synthetic_drag_counter += 1; - return; + InputEvent::MouseUp( + event @ MouseUpEvent { + button: MouseButton::Left, + modifiers: Modifiers { control: true, .. }, + .. + }, + ) => { + *event = MouseUpEvent { + button: MouseButton::Right, + modifiers: Modifiers { + control: false, + ..event.modifiers + }, + click_count: 1, + ..*event + }; } - _ => None, + _ => {} }; match &event { @@ -1227,9 +1232,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { if let Some(mut callback) = lock.event_callback.take() { drop(lock); callback(event); - if let Some(event) = synthesized_second_event { - callback(event); - } window_state.lock().event_callback = Some(callback); } } diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 5d9dd5d804376e8f4840c2c4a56f1d1ef35274df..1b0cabb40154575f60ead9d8404bfa7c0fb5ee34 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -203,6 +203,7 @@ impl TextStyle { style: self.font_style, }, color: self.color, + background_color: None, underline: self.underline.clone(), } } diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index c7031fcb4d77c86ebe765c1021468f9751fcc96e..b3d7a96aff9d2eaed8b5c9113be6b8d5f36b8873 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -3,20 +3,20 @@ mod line; mod line_layout; mod line_wrapper; -use anyhow::anyhow; pub use font_features::*; pub use line::*; pub use line_layout::*; pub use line_wrapper::*; -use smallvec::SmallVec; use crate::{ px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size, UnderlineStyle, }; +use anyhow::anyhow; use collections::HashMap; use core::fmt; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; +use smallvec::SmallVec; use std::{ cmp, fmt::{Debug, Display, Formatter}, @@ -151,13 +151,79 @@ impl TextSystem { } } - pub fn layout_text( + pub fn layout_line( &self, text: &str, font_size: Pixels, runs: &[TextRun], + ) -> Result> { + let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); + for run in runs.iter() { + let font_id = self.font_id(&run.font)?; + if let Some(last_run) = font_runs.last_mut() { + if last_run.font_id == font_id { + last_run.len += run.len; + continue; + } + } + font_runs.push(FontRun { + len: run.len, + font_id, + }); + } + + let layout = self + .line_layout_cache + .layout_line(&text, font_size, &font_runs); + + font_runs.clear(); + self.font_runs_pool.lock().push(font_runs); + + Ok(layout) + } + + pub fn shape_line( + &self, + text: SharedString, + font_size: Pixels, + runs: &[TextRun], + ) -> Result { + debug_assert!( + text.find('\n').is_none(), + "text argument should not contain newlines" + ); + + let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); + for run in runs { + if let Some(last_run) = decoration_runs.last_mut() { + if last_run.color == run.color && last_run.underline == run.underline { + last_run.len += run.len as u32; + continue; + } + } + decoration_runs.push(DecorationRun { + len: run.len as u32, + color: run.color, + underline: run.underline.clone(), + }); + } + + let layout = self.layout_line(text.as_ref(), font_size, runs)?; + + Ok(ShapedLine { + layout, + text, + decoration_runs, + }) + } + + pub fn shape_text( + &self, + text: &str, // todo!("pass a SharedString and preserve it when passed a single line?") + font_size: Pixels, + runs: &[TextRun], wrap_width: Option, - ) -> Result> { + ) -> Result> { let mut runs = runs.iter().cloned().peekable(); let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); @@ -210,10 +276,11 @@ impl TextSystem { let layout = self .line_layout_cache - .layout_line(&line_text, font_size, &font_runs, wrap_width); - lines.push(Line { + .layout_wrapped_line(&line_text, font_size, &font_runs, wrap_width); + lines.push(WrappedLine { layout, - decorations: decoration_runs, + decoration_runs, + text: SharedString::from(line_text), }); line_start = line_end + 1; // Skip `\n` character. @@ -384,6 +451,7 @@ pub struct TextRun { pub len: usize, pub font: Font, pub color: Hsla, + pub background_color: Option, pub underline: Option, } diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index 707274ad33f26ce2212512c97df5527bd420ead7..d05ae9468dae491ed2ed130e6773fc1e754a3cf2 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/crates/gpui2/src/text_system/line.rs @@ -1,5 +1,5 @@ use crate::{ - black, point, px, size, BorrowWindow, Bounds, Hsla, Pixels, Point, Result, Size, + black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout, }; use derive_more::{Deref, DerefMut}; @@ -14,23 +14,51 @@ pub struct DecorationRun { } #[derive(Clone, Default, Debug, Deref, DerefMut)] -pub struct Line { +pub struct ShapedLine { #[deref] #[deref_mut] - pub(crate) layout: Arc, - pub(crate) decorations: SmallVec<[DecorationRun; 32]>, + pub(crate) layout: Arc, + pub text: SharedString, + pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, } -impl Line { - pub fn size(&self, line_height: Pixels) -> Size { - size( - self.layout.width, - line_height * (self.layout.wrap_boundaries.len() + 1), - ) +impl ShapedLine { + pub fn len(&self) -> usize { + self.layout.len } - pub fn wrap_count(&self) -> usize { - self.layout.wrap_boundaries.len() + pub fn paint( + &self, + origin: Point, + line_height: Pixels, + cx: &mut WindowContext, + ) -> Result<()> { + paint_line( + origin, + &self.layout, + line_height, + &self.decoration_runs, + None, + &[], + cx, + )?; + + Ok(()) + } +} + +#[derive(Clone, Default, Debug, Deref, DerefMut)] +pub struct WrappedLine { + #[deref] + #[deref_mut] + pub(crate) layout: Arc, + pub text: SharedString, + pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, +} + +impl WrappedLine { + pub fn len(&self) -> usize { + self.layout.len() } pub fn paint( @@ -39,75 +67,50 @@ impl Line { line_height: Pixels, cx: &mut WindowContext, ) -> Result<()> { - let padding_top = - (line_height - self.layout.layout.ascent - self.layout.layout.descent) / 2.; - let baseline_offset = point(px(0.), padding_top + self.layout.layout.ascent); - - let mut style_runs = self.decorations.iter(); - let mut wraps = self.layout.wrap_boundaries.iter().peekable(); - let mut run_end = 0; - let mut color = black(); - let mut current_underline: Option<(Point, UnderlineStyle)> = None; - let text_system = cx.text_system().clone(); - - let mut glyph_origin = origin; - let mut prev_glyph_position = Point::default(); - for (run_ix, run) in self.layout.layout.runs.iter().enumerate() { - let max_glyph_size = text_system - .bounding_box(run.font_id, self.layout.layout.font_size)? - .size; - - for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { - glyph_origin.x += glyph.position.x - prev_glyph_position.x; - - if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { - wraps.next(); - if let Some((underline_origin, underline_style)) = current_underline.take() { - cx.paint_underline( - underline_origin, - glyph_origin.x - underline_origin.x, - &underline_style, - )?; - } - - glyph_origin.x = origin.x; - glyph_origin.y += line_height; - } - prev_glyph_position = glyph.position; - - let mut finished_underline: Option<(Point, UnderlineStyle)> = None; - if glyph.index >= run_end { - if let Some(style_run) = style_runs.next() { - if let Some((_, underline_style)) = &mut current_underline { - if style_run.underline.as_ref() != Some(underline_style) { - finished_underline = current_underline.take(); - } - } - if let Some(run_underline) = style_run.underline.as_ref() { - current_underline.get_or_insert(( - point( - glyph_origin.x, - origin.y - + baseline_offset.y - + (self.layout.layout.descent * 0.618), - ), - UnderlineStyle { - color: Some(run_underline.color.unwrap_or(style_run.color)), - thickness: run_underline.thickness, - wavy: run_underline.wavy, - }, - )); - } + paint_line( + origin, + &self.layout.unwrapped_layout, + line_height, + &self.decoration_runs, + self.wrap_width, + &self.wrap_boundaries, + cx, + )?; - run_end += style_run.len as usize; - color = style_run.color; - } else { - run_end = self.layout.text.len(); - finished_underline = current_underline.take(); - } - } + Ok(()) + } +} - if let Some((underline_origin, underline_style)) = finished_underline { +fn paint_line( + origin: Point, + layout: &LineLayout, + line_height: Pixels, + decoration_runs: &[DecorationRun], + wrap_width: Option, + wrap_boundaries: &[WrapBoundary], + cx: &mut WindowContext<'_>, +) -> Result<()> { + let padding_top = (line_height - layout.ascent - layout.descent) / 2.; + let baseline_offset = point(px(0.), padding_top + layout.ascent); + let mut decoration_runs = decoration_runs.iter(); + let mut wraps = wrap_boundaries.iter().peekable(); + let mut run_end = 0; + let mut color = black(); + let mut current_underline: Option<(Point, UnderlineStyle)> = None; + let text_system = cx.text_system().clone(); + let mut glyph_origin = origin; + let mut prev_glyph_position = Point::default(); + for (run_ix, run) in layout.runs.iter().enumerate() { + let max_glyph_size = text_system + .bounding_box(run.font_id, layout.font_size)? + .size; + + for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + glyph_origin.x += glyph.position.x - prev_glyph_position.x; + + if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { + wraps.next(); + if let Some((underline_origin, underline_style)) = current_underline.take() { cx.paint_underline( underline_origin, glyph_origin.x - underline_origin.x, @@ -115,42 +118,84 @@ impl Line { )?; } - let max_glyph_bounds = Bounds { - origin: glyph_origin, - size: max_glyph_size, - }; - - let content_mask = cx.content_mask(); - if max_glyph_bounds.intersects(&content_mask.bounds) { - if glyph.is_emoji { - cx.paint_emoji( - glyph_origin + baseline_offset, - run.font_id, - glyph.id, - self.layout.layout.font_size, - )?; - } else { - cx.paint_glyph( - glyph_origin + baseline_offset, - run.font_id, - glyph.id, - self.layout.layout.font_size, - color, - )?; + glyph_origin.x = origin.x; + glyph_origin.y += line_height; + } + prev_glyph_position = glyph.position; + + let mut finished_underline: Option<(Point, UnderlineStyle)> = None; + if glyph.index >= run_end { + if let Some(style_run) = decoration_runs.next() { + if let Some((_, underline_style)) = &mut current_underline { + if style_run.underline.as_ref() != Some(underline_style) { + finished_underline = current_underline.take(); + } } + if let Some(run_underline) = style_run.underline.as_ref() { + current_underline.get_or_insert(( + point( + glyph_origin.x, + origin.y + baseline_offset.y + (layout.descent * 0.618), + ), + UnderlineStyle { + color: Some(run_underline.color.unwrap_or(style_run.color)), + thickness: run_underline.thickness, + wavy: run_underline.wavy, + }, + )); + } + + run_end += style_run.len as usize; + color = style_run.color; + } else { + run_end = layout.len; + finished_underline = current_underline.take(); } } - } - if let Some((underline_start, underline_style)) = current_underline.take() { - let line_end_x = origin.x + self.layout.layout.width; - cx.paint_underline( - underline_start, - line_end_x - underline_start.x, - &underline_style, - )?; + if let Some((underline_origin, underline_style)) = finished_underline { + cx.paint_underline( + underline_origin, + glyph_origin.x - underline_origin.x, + &underline_style, + )?; + } + + let max_glyph_bounds = Bounds { + origin: glyph_origin, + size: max_glyph_size, + }; + + let content_mask = cx.content_mask(); + if max_glyph_bounds.intersects(&content_mask.bounds) { + if glyph.is_emoji { + cx.paint_emoji( + glyph_origin + baseline_offset, + run.font_id, + glyph.id, + layout.font_size, + )?; + } else { + cx.paint_glyph( + glyph_origin + baseline_offset, + run.font_id, + glyph.id, + layout.font_size, + color, + )?; + } + } } + } - Ok(()) + if let Some((underline_start, underline_style)) = current_underline.take() { + let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width); + cx.paint_underline( + underline_start, + line_end_x - underline_start.x, + &underline_style, + )?; } + + Ok(()) } diff --git a/crates/gpui2/src/text_system/line_layout.rs b/crates/gpui2/src/text_system/line_layout.rs index 7e9176cacaeedf61234a42bb02bd48fd2eae398f..a5cf814a8c24e36d5c41217f7db5aa528452d312 100644 --- a/crates/gpui2/src/text_system/line_layout.rs +++ b/crates/gpui2/src/text_system/line_layout.rs @@ -1,5 +1,4 @@ -use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString}; -use derive_more::{Deref, DerefMut}; +use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size}; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use smallvec::SmallVec; use std::{ @@ -149,13 +148,11 @@ impl LineLayout { } } -#[derive(Deref, DerefMut, Default, Debug)] +#[derive(Default, Debug)] pub struct WrappedLineLayout { - #[deref] - #[deref_mut] - pub layout: LineLayout, - pub text: SharedString, + pub unwrapped_layout: Arc, pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>, + pub wrap_width: Option, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -164,31 +161,74 @@ pub struct WrapBoundary { pub glyph_ix: usize, } +impl WrappedLineLayout { + pub fn len(&self) -> usize { + self.unwrapped_layout.len + } + + pub fn width(&self) -> Pixels { + self.wrap_width + .unwrap_or(Pixels::MAX) + .min(self.unwrapped_layout.width) + } + + pub fn size(&self, line_height: Pixels) -> Size { + Size { + width: self.width(), + height: line_height * (self.wrap_boundaries.len() + 1), + } + } + + pub fn ascent(&self) -> Pixels { + self.unwrapped_layout.ascent + } + + pub fn descent(&self) -> Pixels { + self.unwrapped_layout.descent + } + + pub fn wrap_boundaries(&self) -> &[WrapBoundary] { + &self.wrap_boundaries + } + + pub fn font_size(&self) -> Pixels { + self.unwrapped_layout.font_size + } + + pub fn runs(&self) -> &[ShapedRun] { + &self.unwrapped_layout.runs + } +} + pub(crate) struct LineLayoutCache { - prev_frame: Mutex>>, - curr_frame: RwLock>>, + previous_frame: Mutex>>, + current_frame: RwLock>>, + previous_frame_wrapped: Mutex>>, + current_frame_wrapped: RwLock>>, platform_text_system: Arc, } impl LineLayoutCache { pub fn new(platform_text_system: Arc) -> Self { Self { - prev_frame: Mutex::new(HashMap::new()), - curr_frame: RwLock::new(HashMap::new()), + previous_frame: Mutex::default(), + current_frame: RwLock::default(), + previous_frame_wrapped: Mutex::default(), + current_frame_wrapped: RwLock::default(), platform_text_system, } } pub fn start_frame(&self) { - let mut prev_frame = self.prev_frame.lock(); - let mut curr_frame = self.curr_frame.write(); + let mut prev_frame = self.previous_frame.lock(); + let mut curr_frame = self.current_frame.write(); std::mem::swap(&mut *prev_frame, &mut *curr_frame); curr_frame.clear(); } - pub fn layout_line( + pub fn layout_wrapped_line( &self, - text: &SharedString, + text: &str, font_size: Pixels, runs: &[FontRun], wrap_width: Option, @@ -199,34 +239,66 @@ impl LineLayoutCache { runs, wrap_width, } as &dyn AsCacheKeyRef; - let curr_frame = self.curr_frame.upgradable_read(); - if let Some(layout) = curr_frame.get(key) { + + let current_frame = self.current_frame_wrapped.upgradable_read(); + if let Some(layout) = current_frame.get(key) { return layout.clone(); } - let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame); - if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) { - curr_frame.insert(key, layout.clone()); + let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); + if let Some((key, layout)) = self.previous_frame_wrapped.lock().remove_entry(key) { + current_frame.insert(key, layout.clone()); layout } else { - let layout = self.platform_text_system.layout_line(text, font_size, runs); - let wrap_boundaries = wrap_width - .map(|wrap_width| layout.compute_wrap_boundaries(text.as_ref(), wrap_width)) - .unwrap_or_default(); - let wrapped_line = Arc::new(WrappedLineLayout { - layout, - text: text.clone(), + let unwrapped_layout = self.layout_line(text, font_size, runs); + let wrap_boundaries = if let Some(wrap_width) = wrap_width { + unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width) + } else { + SmallVec::new() + }; + let layout = Arc::new(WrappedLineLayout { + unwrapped_layout, wrap_boundaries, + wrap_width, }); - let key = CacheKey { - text: text.clone(), + text: text.into(), font_size, runs: SmallVec::from(runs), wrap_width, }; - curr_frame.insert(key, wrapped_line.clone()); - wrapped_line + current_frame.insert(key, layout.clone()); + layout + } + } + + pub fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> Arc { + let key = &CacheKeyRef { + text, + font_size, + runs, + wrap_width: None, + } as &dyn AsCacheKeyRef; + + let current_frame = self.current_frame.upgradable_read(); + if let Some(layout) = current_frame.get(key) { + return layout.clone(); + } + + let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); + if let Some((key, layout)) = self.previous_frame.lock().remove_entry(key) { + current_frame.insert(key, layout.clone()); + layout + } else { + let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs)); + let key = CacheKey { + text: text.into(), + font_size, + runs: SmallVec::from(runs), + wrap_width: None, + }; + current_frame.insert(key, layout.clone()); + layout } } } @@ -243,7 +315,7 @@ trait AsCacheKeyRef { #[derive(Eq)] struct CacheKey { - text: SharedString, + text: String, font_size: Pixels, runs: SmallVec<[FontRun; 1]>, wrap_width: Option, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index ff4c13abce5148768ba89549370173b6b34cb44a..6d07f06d9441b838828f7cf15ab0c2a6da72ff4e 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -185,10 +185,27 @@ impl Drop for FocusHandle { } } +/// FocusableView allows users of your view to easily +/// focus it (using cx.focus_view(view)) pub trait FocusableView: Render { fn focus_handle(&self, cx: &AppContext) -> FocusHandle; } +/// ManagedView is a view (like a Modal, Popover, Menu, etc.) +/// where the lifecycle of the view is handled by another view. +pub trait ManagedView: Render { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle; +} + +pub struct Dismiss; +impl EventEmitter for T {} + +impl FocusableView for T { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.focus_handle(cx) + } +} + // Holds the state for a specific window. pub struct Window { pub(crate) handle: AnyWindowHandle, @@ -574,6 +591,7 @@ impl<'a> WindowContext<'a> { result } + #[must_use] /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which /// layout is being requested, along with the layout ids of any children. This method is called during /// calls to the `Element::layout` trait method and enables any element to participate in layout. @@ -1150,6 +1168,14 @@ impl<'a> WindowContext<'a> { self.window.mouse_position = mouse_move.position; InputEvent::MouseMove(mouse_move) } + InputEvent::MouseDown(mouse_down) => { + self.window.mouse_position = mouse_down.position; + InputEvent::MouseDown(mouse_down) + } + InputEvent::MouseUp(mouse_up) => { + self.window.mouse_position = mouse_up.position; + InputEvent::MouseUp(mouse_up) + } // Translate dragging and dropping of external files from the operating system // to internal drag and drop events. InputEvent::FileDrop(file_drop) => match file_drop { diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 72a2f812e974d7f45d5b1110d4c26ac093e78f29..3491fc3d4a1c7bbbbe1d7e1f0c2cc2304e852bca 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,7 +1,7 @@ use editor::Editor; use gpui::{ - div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task, - UniformListScrollHandle, View, ViewContext, WindowContext, + div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView, + MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, }; use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Divider, Label, TextColor}; @@ -35,6 +35,12 @@ pub trait PickerDelegate: Sized + 'static { ) -> Self::ListItem; } +impl FocusableView for Picker { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.editor.focus_handle(cx) + } +} + impl Picker { pub fn new(delegate: D, cx: &mut ViewContext) -> Self { let editor = cx.build_view(|cx| { diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index 20adc44c1aa84ec96491ffacd884de5b73efbfb2..a0bc7cd72f10e25fc68674071afce253582cacf9 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -66,7 +66,6 @@ fn main() { story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace)); let theme_registry = cx.global::(); - let mut theme_settings = ThemeSettings::get_global(cx).clone(); theme_settings.active_theme = theme_registry.get(&theme_name).unwrap(); ThemeSettings::override_global(theme_settings, cx); @@ -114,6 +113,7 @@ impl Render for StoryWrapper { .flex() .flex_col() .size_full() + .font("Zed Mono") .child(self.story.clone()) } } diff --git a/crates/storybook3/Cargo.toml b/crates/storybook3/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..8b04e4d44b306969de63a7bd880d61b724aed32b --- /dev/null +++ b/crates/storybook3/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "storybook3" +version = "0.1.0" +edition = "2021" +publish = false + +[[bin]] +name = "storybook" +path = "src/storybook3.rs" + +[dependencies] +anyhow.workspace = true + +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2", features = ["stories"] } +theme = { package = "theme2", path = "../theme2", features = ["stories"] } +settings = { package = "settings2", path = "../settings2"} diff --git a/crates/storybook3/src/storybook3.rs b/crates/storybook3/src/storybook3.rs new file mode 100644 index 0000000000000000000000000000000000000000..291f8ce2ac451ca7c49e72776fcd4b7411644c6d --- /dev/null +++ b/crates/storybook3/src/storybook3.rs @@ -0,0 +1,73 @@ +use anyhow::Result; +use gpui::AssetSource; +use gpui::{ + div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds, + WindowOptions, +}; +use settings::{default_settings, Settings, SettingsStore}; +use std::borrow::Cow; +use std::sync::Arc; +use theme::ThemeSettings; +use ui::{prelude::*, ContextMenuStory}; + +struct Assets; + +impl AssetSource for Assets { + fn load(&self, _path: &str) -> Result> { + todo!(); + } + + fn list(&self, _path: &str) -> Result> { + Ok(vec![]) + } +} + +fn main() { + let asset_source = Arc::new(Assets); + gpui::App::production(asset_source).run(move |cx| { + let mut store = SettingsStore::default(); + store + .set_default_settings(default_settings().as_ref(), cx) + .unwrap(); + cx.set_global(store); + ui::settings::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); + + cx.open_window( + WindowOptions { + bounds: WindowBounds::Fixed(Bounds { + origin: Default::default(), + size: size(px(1500.), px(780.)).into(), + }), + ..Default::default() + }, + move |cx| { + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + cx.set_rem_size(ui_font_size); + + cx.build_view(|cx| TestView { + story: cx.build_view(|_| ContextMenuStory).into(), + }) + }, + ); + + cx.activate(true); + }) +} + +struct TestView { + story: AnyView, +} + +impl Render for TestView { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + div() + .flex() + .flex_col() + .size_full() + .font("Helvetica") + .child(self.story.clone()) + } +} diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index f815dbe0ea9a05d95880917eae255ef63c797546..b6ab7e86b9191fa6910e5632158ae0c587059c21 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -32,7 +32,7 @@ use workspace::{ notifications::NotifyResultExt, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem}, - ui::{ContextMenu, ContextMenuItem, Label}, + ui::{ContextMenu, Label}, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; @@ -85,7 +85,7 @@ pub struct TerminalView { has_new_content: bool, //Currently using iTerm bell, show bell emoji in tab until input is received has_bell: bool, - context_menu: Option, + context_menu: Option>, blink_state: bool, blinking_on: bool, blinking_paused: bool, @@ -300,10 +300,14 @@ impl TerminalView { position: gpui::Point, cx: &mut ViewContext, ) { - self.context_menu = Some(ContextMenu::new(vec![ - ContextMenuItem::entry(Label::new("Clear"), Clear), - ContextMenuItem::entry(Label::new("Close"), CloseActiveItem { save_intent: None }), - ])); + self.context_menu = Some(cx.build_view(|cx| { + ContextMenu::new(cx) + .entry(Label::new("Clear"), Box::new(Clear)) + .entry( + Label::new("Close"), + Box::new(CloseActiveItem { save_intent: None }), + ) + })); dbg!(&position); // todo!() // self.context_menu diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 6b3371e33861c5475e42596b9de6b57afd183617..d3214cbff1b31d7fda3c8fe80a108a55490f2119 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,82 +1,258 @@ -use crate::{prelude::*, ListItemVariant}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::prelude::*; use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader}; +use gpui::{ + overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div, + FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, +}; -pub enum ContextMenuItem { - Header(SharedString), - Entry(Label, Box), - Separator, +pub struct ContextMenu { + items: Vec, + focus_handle: FocusHandle, } -impl Clone for ContextMenuItem { - fn clone(&self) -> Self { - match self { - ContextMenuItem::Header(name) => ContextMenuItem::Header(name.clone()), - ContextMenuItem::Entry(label, action) => { - ContextMenuItem::Entry(label.clone(), action.boxed_clone()) - } - ContextMenuItem::Separator => ContextMenuItem::Separator, - } +impl ManagedView for ContextMenu { + fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle { + self.focus_handle.clone() } } -impl ContextMenuItem { - fn to_list_item(self) -> ListItem { - match self { - ContextMenuItem::Header(label) => ListSubHeader::new(label).into(), - ContextMenuItem::Entry(label, action) => ListEntry::new(label) - .variant(ListItemVariant::Inset) - .on_click(action) - .into(), - ContextMenuItem::Separator => ListSeparator::new().into(), + +impl ContextMenu { + pub fn new(cx: &mut WindowContext) -> Self { + Self { + items: Default::default(), + focus_handle: cx.focus_handle(), } } - pub fn header(label: impl Into) -> Self { - Self::Header(label.into()) + pub fn header(mut self, title: impl Into) -> Self { + self.items.push(ListItem::Header(ListSubHeader::new(title))); + self + } + + pub fn separator(mut self) -> Self { + self.items.push(ListItem::Separator(ListSeparator)); + self + } + + pub fn entry(mut self, label: Label, action: Box) -> Self { + self.items.push(ListEntry::new(label).action(action).into()); + self } - pub fn separator() -> Self { - Self::Separator + pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + // todo!() + cx.emit(Dismiss); } - pub fn entry(label: Label, action: impl Action) -> Self { - Self::Entry(label, Box::new(action)) + pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(Dismiss); } } -#[derive(Component, Clone)] -pub struct ContextMenu { - items: Vec, +impl Render for ContextMenu { + type Element = Div; + // todo!() + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().elevation_2(cx).flex().flex_row().child( + v_stack() + .min_w(px(200.)) + .track_focus(&self.focus_handle) + .on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx)) + // .on_action(ContextMenu::select_first) + // .on_action(ContextMenu::select_last) + // .on_action(ContextMenu::select_next) + // .on_action(ContextMenu::select_prev) + .on_action(ContextMenu::confirm) + .on_action(ContextMenu::cancel) + .flex_none() + // .bg(cx.theme().colors().elevated_surface_background) + // .border() + // .border_color(cx.theme().colors().border) + .child(List::new(self.items.clone())), + ) + } } -impl ContextMenu { - pub fn new(items: impl IntoIterator) -> Self { - Self { - items: items.into_iter().collect(), +pub struct MenuHandle { + id: Option, + child_builder: Option AnyElement + 'static>>, + menu_builder: Option) -> View + 'static>>, + + anchor: Option, + attach: Option, +} + +impl MenuHandle { + pub fn id(mut self, id: impl Into) -> Self { + self.id = Some(id.into()); + self + } + + pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext) -> View + 'static) -> Self { + self.menu_builder = Some(Rc::new(f)); + self + } + + pub fn child>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self { + self.child_builder = Some(Box::new(|b| f(b).render())); + self + } + + /// anchor defines which corner of the menu to anchor to the attachment point + /// (by default the cursor position, but see attach) + pub fn anchor(mut self, anchor: AnchorCorner) -> Self { + self.anchor = Some(anchor); + self + } + + /// attach defines which corner of the handle to attach the menu's anchor to + pub fn attach(mut self, attach: AnchorCorner) -> Self { + self.attach = Some(attach); + self + } +} + +pub fn menu_handle() -> MenuHandle { + MenuHandle { + id: None, + child_builder: None, + menu_builder: None, + anchor: None, + attach: None, + } +} + +pub struct MenuHandleState { + menu: Rc>>>, + position: Rc>>, + child_layout_id: Option, + child_element: Option>, + menu_element: Option>, +} +impl Element for MenuHandle { + type ElementState = MenuHandleState; + + fn element_id(&self) -> Option { + Some(self.id.clone().expect("menu_handle must have an id()")) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut crate::ViewContext, + ) -> (gpui::LayoutId, Self::ElementState) { + let (menu, position) = if let Some(element_state) = element_state { + (element_state.menu, element_state.position) + } else { + (Rc::default(), Rc::default()) + }; + + let mut menu_layout_id = None; + + let menu_element = menu.borrow_mut().as_mut().map(|menu| { + let mut overlay = overlay::().snap_to_window(); + if let Some(anchor) = self.anchor { + overlay = overlay.anchor(anchor); + } + overlay = overlay.position(*position.borrow()); + + let mut view = overlay.child(menu.clone()).render(); + menu_layout_id = Some(view.layout(view_state, cx)); + view + }); + + let mut child_element = self + .child_builder + .take() + .map(|child_builder| (child_builder)(menu.borrow().is_some())); + + let child_layout_id = child_element + .as_mut() + .map(|child_element| child_element.layout(view_state, cx)); + + let layout_id = cx.request_layout( + &gpui::Style::default(), + menu_layout_id.into_iter().chain(child_layout_id), + ); + + ( + layout_id, + MenuHandleState { + menu, + position, + child_element, + child_layout_id, + menu_element, + }, + ) + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut crate::ViewContext, + ) { + if let Some(child) = element_state.child_element.as_mut() { + child.paint(view_state, cx); } + + if let Some(menu) = element_state.menu_element.as_mut() { + menu.paint(view_state, cx); + return; + } + + let Some(builder) = self.menu_builder.clone() else { + return; + }; + let menu = element_state.menu.clone(); + let position = element_state.position.clone(); + let attach = self.attach.clone(); + let child_layout_id = element_state.child_layout_id.clone(); + + cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && event.button == MouseButton::Right + && bounds.contains_point(&event.position) + { + cx.stop_propagation(); + cx.prevent_default(); + + let new_menu = (builder)(view_state, cx); + let menu2 = menu.clone(); + cx.subscribe(&new_menu, move |this, modal, e, cx| match e { + &Dismiss => { + *menu2.borrow_mut() = None; + cx.notify(); + } + }) + .detach(); + *menu.borrow_mut() = Some(new_menu); + + *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() { + attach + .unwrap() + .corner(cx.layout_bounds(child_layout_id.unwrap())) + } else { + cx.mouse_position() + }; + cx.notify(); + } + }); } - // todo!() - // cx.add_action(ContextMenu::select_first); - // cx.add_action(ContextMenu::select_last); - // cx.add_action(ContextMenu::select_next); - // cx.add_action(ContextMenu::select_prev); - // cx.add_action(ContextMenu::confirm); - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - v_stack() - .flex() - .bg(cx.theme().colors().elevated_surface_background) - .border() - .border_color(cx.theme().colors().border) - .child(List::new( - self.items - .into_iter() - .map(ContextMenuItem::to_list_item::) - .collect(), - )) - .on_mouse_down_out(|_, _, cx| cx.dispatch_action(Box::new(menu::Cancel))) +} + +impl Component for MenuHandle { + fn render(self) -> AnyElement { + AnyElement::new(self) } } -use gpui::Action; #[cfg(feature = "stories")] pub use stories::*; @@ -84,8 +260,18 @@ pub use stories::*; mod stories { use super::*; use crate::story::Story; - use gpui::{Div, Render}; - use serde::Deserialize; + use gpui::{actions, Div, Render, VisualContext}; + + actions!(PrintCurrentDate); + + fn build_menu(cx: &mut WindowContext, header: impl Into) -> View { + cx.build_view(|cx| { + ContextMenu::new(cx).header(header).separator().entry( + Label::new("Print current time"), + PrintCurrentDate.boxed_clone(), + ) + }) + } pub struct ContextMenuStory; @@ -93,22 +279,84 @@ mod stories { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - #[derive(PartialEq, Clone, Deserialize, gpui::Action)] - struct PrintCurrentDate {} - Story::container(cx) - .child(Story::title_for::<_, ContextMenu>(cx)) - .child(Story::label(cx, "Default")) - .child(ContextMenu::new([ - ContextMenuItem::header("Section header"), - ContextMenuItem::Separator, - ContextMenuItem::entry(Label::new("Print current time"), PrintCurrentDate {}), - ])) .on_action(|_, _: &PrintCurrentDate, _| { if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() { println!("Current Unix time is {:?}", unix_time.as_secs()); } }) + .flex() + .flex_row() + .justify_between() + .child( + div() + .flex() + .flex_col() + .justify_between() + .child( + menu_handle() + .id("test2") + .child(|is_open| { + Label::new(if is_open { + "TOP LEFT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .menu(move |_, cx| build_menu(cx, "top left")), + ) + .child( + menu_handle() + .id("test1") + .child(|is_open| { + Label::new(if is_open { + "BOTTOM LEFT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .anchor(AnchorCorner::BottomLeft) + .attach(AnchorCorner::TopLeft) + .menu(move |_, cx| build_menu(cx, "bottom left")), + ), + ) + .child( + div() + .flex() + .flex_col() + .justify_between() + .child( + menu_handle() + .id("test3") + .child(|is_open| { + Label::new(if is_open { + "TOP RIGHT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .anchor(AnchorCorner::TopRight) + .menu(move |_, cx| build_menu(cx, "top right")), + ) + .child( + menu_handle() + .id("test4") + .child(|is_open| { + Label::new(if is_open { + "BOTTOM RIGHT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .anchor(AnchorCorner::BottomRight) + .attach(AnchorCorner::TopRight) + .menu(move |_, cx| build_menu(cx, "bottom right")), + ), + ) } } } diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 5512da2b34638e25e6450fd92f59b4a8d60c23f6..9b8548e3f9c6cf092f08f41bbda3bfc25293bc98 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,5 +1,5 @@ use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement}; -use gpui::{prelude::*, AnyView, MouseButton}; +use gpui::{prelude::*, Action, AnyView, MouseButton}; use std::sync::Arc; struct IconButtonHandlers { @@ -19,6 +19,7 @@ pub struct IconButton { color: TextColor, variant: ButtonVariant, state: InteractionState, + selected: bool, tooltip: Option) -> AnyView + 'static>>, handlers: IconButtonHandlers, } @@ -31,6 +32,7 @@ impl IconButton { color: TextColor::default(), variant: ButtonVariant::default(), state: InteractionState::default(), + selected: false, tooltip: None, handlers: IconButtonHandlers::default(), } @@ -56,6 +58,11 @@ impl IconButton { self } + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + pub fn tooltip( mut self, tooltip: impl Fn(&mut V, &mut ViewContext) -> AnyView + 'static, @@ -69,6 +76,10 @@ impl IconButton { self } + pub fn action(self, action: Box) -> Self { + self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone())) + } + fn render(mut self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let icon_color = match (self.state, self.color) { (InteractionState::Disabled, _) => TextColor::Disabled, @@ -76,7 +87,7 @@ impl IconButton { _ => self.color, }; - let (bg_color, bg_hover_color, bg_active_color) = match self.variant { + let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant { ButtonVariant::Filled => ( cx.theme().colors().element_background, cx.theme().colors().element_hover, @@ -89,6 +100,10 @@ impl IconButton { ), }; + if self.selected { + bg_color = bg_hover_color; + } + let mut button = h_stack() .id(self.id.clone()) .justify_center() @@ -108,7 +123,9 @@ impl IconButton { } if let Some(tooltip) = self.tooltip.take() { - button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx)) + if !self.selected { + button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx)) + } } button diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 4b355dd5b6c159c906117e64b3008f8d100129c7..b9508c54136aa424789f943ef40cf47d58122fae 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -117,7 +117,7 @@ impl ListHeader { } } -#[derive(Component)] +#[derive(Component, Clone)] pub struct ListSubHeader { label: SharedString, left_icon: Option, @@ -172,7 +172,7 @@ pub enum ListEntrySize { Medium, } -#[derive(Component)] +#[derive(Component, Clone)] pub enum ListItem { Entry(ListEntry), Separator(ListSeparator), @@ -234,6 +234,24 @@ pub struct ListEntry { on_click: Option>, } +impl Clone for ListEntry { + fn clone(&self) -> Self { + Self { + disabled: self.disabled, + // TODO: Reintroduce this + // disclosure_control_style: DisclosureControlVisibility, + indent_level: self.indent_level, + label: self.label.clone(), + left_slot: self.left_slot.clone(), + overflow: self.overflow, + size: self.size, + toggle: self.toggle, + variant: self.variant, + on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()), + } + } +} + impl ListEntry { pub fn new(label: Label) -> Self { Self { @@ -249,7 +267,7 @@ impl ListEntry { } } - pub fn on_click(mut self, action: impl Into>) -> Self { + pub fn action(mut self, action: impl Into>) -> Self { self.on_click = Some(action.into()); self } diff --git a/crates/ui2/src/story.rs b/crates/ui2/src/story.rs index 94e38267f4c5b160365317c4e597a76748c05d6f..c98cfa012f261c8a3e77251370e2265e057158ec 100644 --- a/crates/ui2/src/story.rs +++ b/crates/ui2/src/story.rs @@ -12,7 +12,6 @@ impl Story { .flex_col() .pt_2() .px_4() - .font("Zed Mono") .bg(cx.theme().colors().background) } diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index d9911e683358dfd37a9110f71cf3899debe52904..9037682807c0685ab7cf844fbed9c6737e30e83f 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -5,6 +5,7 @@ use crate::{ElevationIndex, UITextSize}; fn elevated(this: E, cx: &mut ViewContext, index: ElevationIndex) -> E { this.bg(cx.theme().colors().elevated_surface_background) + .z_index(index.z_index()) .rounded_lg() .border() .border_color(cx.theme().colors().border_variant) diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 64da42cea7a4a197a8e7e474ea1f5b75160c4951..9603875aed3acf68e803deba98f8946cacb024fd 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,14 +1,14 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, FocusableView, ParentComponent, Render, Styled, Subscription, View, ViewContext, - WeakView, WindowContext, + div, px, Action, AnchorCorner, AnyView, AppContext, Component, Div, Entity, EntityId, + EventEmitter, FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled, + Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; use theme2::ActiveTheme; -use ui::{h_stack, IconButton, InteractionState, Tooltip}; +use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip}; pub enum PanelEvent { ChangePosition, @@ -416,6 +416,14 @@ impl Dock { cx.notify(); } } + + pub fn toggle_action(&self) -> Box { + match self.position { + DockPosition::Left => crate::ToggleLeftDock.boxed_clone(), + DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(), + DockPosition::Right => crate::ToggleRightDock.boxed_clone(), + } + } } impl Render for Dock { @@ -603,6 +611,7 @@ impl PanelButtons { // } // } +// here be kittens impl Render for PanelButtons { type Element = Div; @@ -612,6 +621,13 @@ impl Render for PanelButtons { let active_index = dock.active_panel_index; let is_open = dock.is_open; + let (menu_anchor, menu_attach) = match dock.position { + DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft), + DockPosition::Bottom | DockPosition::Right => { + (AnchorCorner::BottomRight, AnchorCorner::TopRight) + } + }; + let buttons = dock .panel_entries .iter() @@ -619,15 +635,33 @@ impl Render for PanelButtons { .filter_map(|(i, panel)| { let icon = panel.panel.icon(cx)?; let name = panel.panel.persistent_name(); - let action = panel.panel.toggle_action(cx); - let action2 = action.boxed_clone(); - - let mut button = IconButton::new(panel.panel.persistent_name(), icon) - .when(i == active_index, |el| el.state(InteractionState::Active)) - .on_click(move |this, cx| cx.dispatch_action(action.boxed_clone())) - .tooltip(move |_, cx| Tooltip::for_action(name, &*action2, cx)); - Some(button) + let mut button: IconButton = if i == active_index && is_open { + let action = dock.toggle_action(); + let tooltip: SharedString = + format!("Close {} dock", dock.position.to_label()).into(); + IconButton::new(name, icon) + .state(InteractionState::Active) + .action(action.boxed_clone()) + .tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx)) + } else { + let action = panel.panel.toggle_action(cx); + + IconButton::new(name, icon) + .action(action.boxed_clone()) + .tooltip(move |_, cx| Tooltip::for_action(name, &*action, cx)) + }; + + Some( + menu_handle() + .id(name) + .menu(move |_, cx| { + cx.build_view(|cx| ContextMenu::new(cx).header("SECTION")) + }) + .anchor(menu_anchor) + .attach(menu_attach) + .child(|is_open| button.selected(is_open)), + ) }); h_stack().gap_0p5().children(buttons) diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index cd5995d65e438c98264d7d1bd44747d1ce72caf6..8afd8317f94ed5452e49106c50b5e69f056a6e6e 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,6 +1,6 @@ use gpui::{ - div, prelude::*, px, AnyView, Div, EventEmitter, FocusHandle, Render, Subscription, View, - ViewContext, WindowContext, + div, prelude::*, px, AnyView, Div, FocusHandle, ManagedView, Render, Subscription, View, + ViewContext, }; use ui::{h_stack, v_stack}; @@ -15,14 +15,6 @@ pub struct ModalLayer { active_modal: Option, } -pub trait Modal: Render + EventEmitter { - fn focus(&self, cx: &mut WindowContext); -} - -pub enum ModalEvent { - Dismissed, -} - impl ModalLayer { pub fn new() -> Self { Self { active_modal: None } @@ -30,7 +22,7 @@ impl ModalLayer { pub fn toggle_modal(&mut self, cx: &mut ViewContext, build_view: B) where - V: Modal, + V: ManagedView, B: FnOnce(&mut ViewContext) -> V, { if let Some(active_modal) = &self.active_modal { @@ -46,17 +38,15 @@ impl ModalLayer { pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) where - V: Modal, + V: ManagedView, { self.active_modal = Some(ActiveModal { modal: new_modal.clone().into(), - subscription: cx.subscribe(&new_modal, |this, modal, e, cx| match e { - ModalEvent::Dismissed => this.hide_modal(cx), - }), + subscription: cx.subscribe(&new_modal, |this, modal, e, cx| this.hide_modal(cx)), previous_focus_handle: cx.focused(), focus_handle: cx.focus_handle(), }); - new_modal.update(cx, |modal, cx| modal.focus(cx)); + cx.focus_view(&new_modal); cx.notify(); } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index c7a27848cea91c776221b0ecf1a123c48d80561b..08d248f6f2a2e3fb1cb266c5c448e4824401af59 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -31,10 +31,10 @@ use futures::{ use gpui::{ actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, - FocusableView, GlobalPixels, InteractiveComponent, KeyContext, Model, ModelContext, - ParentComponent, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, Subscription, - Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, - WindowOptions, + FocusableView, GlobalPixels, InteractiveComponent, KeyContext, ManagedView, Model, + ModelContext, ParentComponent, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, + Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, + WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3202,8 +3202,9 @@ impl Workspace { }) } - fn actions(div: Div) -> Div { - div.on_action(Self::open) + fn actions(&self, div: Div) -> Div { + self.add_workspace_actions_listeners(div) + // cx.add_async_action(Workspace::open); // cx.add_async_action(Workspace::follow_next_collaborator); // cx.add_async_action(Workspace::close); .on_action(Self::close_inactive_items_and_panes) @@ -3239,15 +3240,15 @@ impl Workspace { .on_action(|this, e: &ToggleLeftDock, cx| { this.toggle_dock(DockPosition::Left, cx); }) - // cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { - // workspace.toggle_dock(DockPosition::Right, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { - // workspace.toggle_dock(DockPosition::Bottom, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { - // workspace.close_all_docks(cx); - // }); + .on_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { + workspace.toggle_dock(DockPosition::Right, cx); + }) + .on_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { + workspace.toggle_dock(DockPosition::Bottom, cx); + }) + .on_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { + workspace.close_all_docks(cx); + }) // cx.add_action(Workspace::activate_pane_at_index); // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { // workspace.reopen_closed_item(cx).detach(); @@ -3363,11 +3364,14 @@ impl Workspace { div } - pub fn active_modal(&mut self, cx: &ViewContext) -> Option> { + pub fn active_modal( + &mut self, + cx: &ViewContext, + ) -> Option> { self.modal_layer.read(cx).active_modal() } - pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) where B: FnOnce(&mut ViewContext) -> V, { @@ -3605,7 +3609,7 @@ impl Render for Workspace { cx.set_rem_size(ui_font_size); - Self::actions(self.add_workspace_actions_listeners(div())) + self.actions(div()) .key_context(context) .relative() .size_full()