diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 9a16a7907e2679e7ae4b5e1750ee8d5a503c76c8..852173db35bb9f01f5baf20b8dda6fecd981def8 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -1,16 +1,10 @@ -use editor::Editor; -use gpui::{ - Context, Element, EventEmitter, Focusable, FontWeight, IntoElement, ParentElement, Render, - StyledText, Subscription, Window, -}; -use itertools::Itertools; -use settings::Settings; -use std::cmp; +use editor::render_breadcrumb_text; +use gpui::{Context, EventEmitter, IntoElement, Render, Subscription, Window}; use theme::ActiveTheme; -use ui::{ButtonLike, ButtonStyle, Label, Tooltip, prelude::*}; +use ui::prelude::*; use workspace::{ - TabBarSettings, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, - item::{BreadcrumbText, ItemEvent, ItemHandle}, + ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, + item::{ItemEvent, ItemHandle}, }; pub struct Breadcrumbs { @@ -37,129 +31,30 @@ impl Breadcrumbs { impl EventEmitter for Breadcrumbs {} -// Potential idea: -// - Rename this to "BreadcrumbToolbar" or something -// - Create a wrapping "Breadcrumb" struct for Vec -// - Implement render for _that_ breadcrumb struct. -// - Call that from here to eliminate much of the logic. -// - This will change the Item interface, so do it only after you're happy with the features thus far impl Render for Breadcrumbs { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - const MAX_SEGMENTS: usize = 12; - let element = h_flex() .id("breadcrumb-container") .flex_grow() .h_8() .overflow_x_scroll() .text_ui(cx); - let Some(active_item) = self.active_item.as_ref() else { - return element; + return element.into_any_element(); }; - // Begin - logic we should copy/move - let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else { - return element; + let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else { + return element.into_any_element(); }; - - let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2); - let suffix_start_ix = cmp::max( - prefix_end_ix, - segments.len().saturating_sub(MAX_SEGMENTS / 2), - ); - - if suffix_start_ix > prefix_end_ix { - segments.splice( - prefix_end_ix..suffix_start_ix, - Some(BreadcrumbText { - text: "⋯".into(), - highlights: None, - font: None, - }), - ); - } - - let highlighted_segments = segments.into_iter().enumerate().map(|(index, segment)| { - let mut text_style = window.text_style(); - if let Some(ref font) = segment.font { - text_style.font_family = font.family.clone(); - text_style.font_features = font.features.clone(); - text_style.font_style = font.style; - text_style.font_weight = font.weight; - } - text_style.color = Color::Muted.color(cx); - - if index == 0 - && !TabBarSettings::get_global(cx).show - && active_item.is_dirty(cx) - && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx) - { - return styled_element; - } - - StyledText::new(segment.text.replace('\n', "⏎")) - .with_default_highlights(&text_style, segment.highlights.unwrap_or_default()) - .into_any() - }); - let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || { - Label::new("›").color(Color::Placeholder).into_any_element() - }); - - let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs); - let prefix_element = active_item.breadcrumb_prefix(window, cx); - - let breadcrumbs = if let Some(prefix) = prefix_element { - h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack) - } else { - breadcrumbs_stack - }; - - match active_item - .downcast::() - .map(|editor| editor.downgrade()) - { - Some(editor) => element.child( - ButtonLike::new("toggle outline view") - .child(breadcrumbs) - .style(ButtonStyle::Transparent) - .on_click({ - let editor = editor.clone(); - move |_, window, cx| { - if let Some((editor, callback)) = editor - .upgrade() - .zip(zed_actions::outline::TOGGLE_OUTLINE.get()) - { - callback(editor.to_any_view(), window, cx); - } - } - }) - .tooltip(move |_window, cx| { - if let Some(editor) = editor.upgrade() { - let focus_handle = editor.read(cx).focus_handle(cx); - Tooltip::for_action_in( - "Show Symbol Outline", - &zed_actions::outline::ToggleOutline, - &focus_handle, - cx, - ) - } else { - Tooltip::for_action( - "Show Symbol Outline", - &zed_actions::outline::ToggleOutline, - cx, - ) - } - }), - ), - None => element - // Match the height and padding of the `ButtonLike` in the other arm. - .h(rems_from_px(22.)) - .pl_1() - .child(breadcrumbs), - } - // End + render_breadcrumb_text( + segments, + prefix_element, + Box::new(active_item.as_ref()), + window, + cx, + ) + .into_any_element() } } @@ -208,46 +103,3 @@ impl ToolbarItemView for Breadcrumbs { self.pane_focused = pane_focused; } } - -fn apply_dirty_filename_style( - segment: &BreadcrumbText, - text_style: &gpui::TextStyle, - cx: &mut Context, -) -> Option { - let text = segment.text.replace('\n', "⏎"); - - let filename_position = std::path::Path::new(&segment.text) - .file_name() - .and_then(|f| { - let filename_str = f.to_string_lossy(); - segment.text.rfind(filename_str.as_ref()) - })?; - - let bold_weight = FontWeight::BOLD; - let default_color = Color::Default.color(cx); - - if filename_position == 0 { - let mut filename_style = text_style.clone(); - filename_style.font_weight = bold_weight; - filename_style.color = default_color; - - return Some( - StyledText::new(text) - .with_default_highlights(&filename_style, []) - .into_any(), - ); - } - - let highlight_style = gpui::HighlightStyle { - font_weight: Some(bold_weight), - color: Some(default_color), - ..Default::default() - }; - - let highlight = vec![(filename_position..text.len(), highlight_style)]; - Some( - StyledText::new(text) - .with_default_highlights(text_style, highlight) - .into_any(), - ) -} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 68be482d0611edaf3401a4da48260396cb03cd20..67a15b20b95b8a19d675d77c9edcec5b4e74a218 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -58,6 +58,7 @@ pub use editor_settings::{ }; pub use element::{ CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, + render_breadcrumb_text, }; pub use git::blame::BlameRenderer; pub use hover_popover::hover_markdown_style; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4f55415e024661385cff8c8e0cf4848cade28b1a..472e5d0ae3fc5a965614db4e1ec017032257fc5f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -41,7 +41,7 @@ use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatu use gpui::{ Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, - DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, + DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, FontWeight, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, KeybindingKeystroke, Length, Modifiers, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, PaintQuad, ParentElement, @@ -3880,7 +3880,7 @@ impl EditorElement { let editor = self.editor.read(cx); let multi_buffer = editor.buffer.read(cx); let is_read_only = self.editor.read(cx).read_only(cx); - let weak_editor = self.editor.downgrade(); + let editor_handle: Box<&dyn ItemHandle> = Box::new(&self.editor); let breadcrumbs = if is_selected { editor.breadcrumbs_inner(cx.theme(), cx) @@ -4100,10 +4100,10 @@ impl EditorElement { ) }) .when_some(breadcrumbs, |then, breadcrumbs| { - then.child(self.render_breadcrumb_text( + then.child(render_breadcrumb_text( breadcrumbs, - None, // TODO gotta figure this out somehow - weak_editor, + None, + editor_handle, window, cx, )) @@ -4261,105 +4261,6 @@ impl EditorElement { }) } - // TODO This has too much code in common with Breadcrumb::render. We should find a way to DRY it. - fn render_breadcrumb_text( - &self, - mut segments: Vec, - prefix: Option, - editor: WeakEntity, - window: &mut Window, - cx: &App, - ) -> impl IntoElement { - const MAX_SEGMENTS: usize = 12; - - let element = h_flex().id("breadcrumb-container").flex_grow().text_ui(cx); - - let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2); - let suffix_start_ix = cmp::max( - prefix_end_ix, - segments.len().saturating_sub(MAX_SEGMENTS / 2), - ); - - if suffix_start_ix > prefix_end_ix { - segments.splice( - prefix_end_ix..suffix_start_ix, - Some(BreadcrumbText { - text: "⋯".into(), - highlights: None, - font: None, - }), - ); - } - - let highlighted_segments = segments.into_iter().enumerate().map(|(_index, segment)| { - let mut text_style = window.text_style(); - if let Some(ref font) = segment.font { - text_style.font_family = font.family.clone(); - text_style.font_features = font.features.clone(); - text_style.font_style = font.style; - text_style.font_weight = font.weight; - } - text_style.color = Color::Muted.color(cx); - - // TODO this shouldn't apply here, but will in the formal breadcrumb (e.g. singleton buffer). Need to resolve the difference. - // if index == 0 - // && !TabBarSettings::get_global(cx).show - // && active_item.is_dirty(cx) - // && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx) - // { - // return styled_element; - // } - - StyledText::new(segment.text.replace('\n', "⏎")) - .with_default_highlights(&text_style, segment.highlights.unwrap_or_default()) - .into_any() - }); - let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || { - Label::new("›").color(Color::Placeholder).into_any_element() - }); - - let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs); - - let breadcrumbs = if let Some(prefix) = prefix { - h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack) - } else { - breadcrumbs_stack - }; - element.child( - ButtonLike::new("toggle outline view") - .child(breadcrumbs) - .style(ButtonStyle::Transparent) - .on_click({ - let editor = editor.clone(); - move |_, window, cx| { - if let Some((editor, callback)) = editor - .upgrade() - .zip(zed_actions::outline::TOGGLE_OUTLINE.get()) - { - callback(editor.to_any_view(), window, cx); - } - } - }) - .tooltip(move |_window, cx| { - if let Some(editor) = editor.upgrade() { - let focus_handle = editor.read(cx).focus_handle(cx); - Tooltip::for_action_in( - "Show Symbol Outline", - &zed_actions::outline::ToggleOutline, - &focus_handle, - cx, - ) - } else { - Tooltip::for_action( - "Show Symbol Outline", - &zed_actions::outline::ToggleOutline, - cx, - ) - } - }), - ) - } - fn render_blocks( &self, rows: Range, @@ -7997,6 +7898,156 @@ impl EditorElement { } } +pub fn render_breadcrumb_text( + mut segments: Vec, + prefix: Option, + active_item: Box<&dyn ItemHandle>, + window: &mut Window, + cx: &App, +) -> impl IntoElement { + const MAX_SEGMENTS: usize = 12; + + let element = h_flex().id("breadcrumb-container").flex_grow().text_ui(cx); + + let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2); + let suffix_start_ix = cmp::max( + prefix_end_ix, + segments.len().saturating_sub(MAX_SEGMENTS / 2), + ); + + if suffix_start_ix > prefix_end_ix { + segments.splice( + prefix_end_ix..suffix_start_ix, + Some(BreadcrumbText { + text: "⋯".into(), + highlights: None, + font: None, + }), + ); + } + + let highlighted_segments = segments.into_iter().enumerate().map(|(index, segment)| { + let mut text_style = window.text_style(); + if let Some(ref font) = segment.font { + text_style.font_family = font.family.clone(); + text_style.font_features = font.features.clone(); + text_style.font_style = font.style; + text_style.font_weight = font.weight; + } + text_style.color = Color::Muted.color(cx); + + if index == 0 + && !workspace::TabBarSettings::get_global(cx).show + && active_item.is_dirty(cx) + && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx) + { + return styled_element; + } + + StyledText::new(segment.text.replace('\n', "⏎")) + .with_default_highlights(&text_style, segment.highlights.unwrap_or_default()) + .into_any() + }); + let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || { + Label::new("›").color(Color::Placeholder).into_any_element() + }); + + let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs); + + let breadcrumbs = if let Some(prefix) = prefix { + h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack) + } else { + breadcrumbs_stack + }; + + match active_item + .downcast::() + .map(|editor| editor.downgrade()) + { + Some(editor) => element.child( + ButtonLike::new("toggle outline view") + .child(breadcrumbs) + .style(ButtonStyle::Transparent) + .on_click({ + let editor = editor.clone(); + move |_, window, cx| { + if let Some((editor, callback)) = editor + .upgrade() + .zip(zed_actions::outline::TOGGLE_OUTLINE.get()) + { + callback(editor.to_any_view(), window, cx); + } + } + }) + .tooltip(move |_window, cx| { + if let Some(editor) = editor.upgrade() { + let focus_handle = editor.read(cx).focus_handle(cx); + Tooltip::for_action_in( + "Show Symbol Outline", + &zed_actions::outline::ToggleOutline, + &focus_handle, + cx, + ) + } else { + Tooltip::for_action( + "Show Symbol Outline", + &zed_actions::outline::ToggleOutline, + cx, + ) + } + }), + ), + None => element + // Match the height and padding of the `ButtonLike` in the other arm. + .h(rems_from_px(22.)) + .pl_1() + .child(breadcrumbs), + } +} + +fn apply_dirty_filename_style( + segment: &BreadcrumbText, + text_style: &gpui::TextStyle, + cx: &App, +) -> Option { + let text = segment.text.replace('\n', "⏎"); + + let filename_position = std::path::Path::new(&segment.text) + .file_name() + .and_then(|f| { + let filename_str = f.to_string_lossy(); + segment.text.rfind(filename_str.as_ref()) + })?; + + let bold_weight = FontWeight::BOLD; + let default_color = Color::Default.color(cx); + + if filename_position == 0 { + let mut filename_style = text_style.clone(); + filename_style.font_weight = bold_weight; + filename_style.color = default_color; + + return Some( + StyledText::new(text) + .with_default_highlights(&filename_style, []) + .into_any(), + ); + } + + let highlight_style = gpui::HighlightStyle { + font_weight: Some(bold_weight), + color: Some(default_color), + ..Default::default() + }; + + let highlight = vec![(filename_position..text.len(), highlight_style)]; + Some( + StyledText::new(text) + .with_default_highlights(text_style, highlight) + .into_any(), + ) +} + fn file_status_label_color(file_status: Option) -> Color { file_status.map_or(Color::Default, |status| { if status.is_conflicted() { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 6e415c23454388bc7931ff9d5e499924d6b8f55d..809111f1e4c14257f375801ff6fa2b2f5e03565c 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -124,6 +124,7 @@ pub enum ItemEvent { } // TODO: Combine this with existing HighlightedText struct? +#[derive(Debug)] pub struct BreadcrumbText { pub text: String, pub highlights: Option, HighlightStyle)>>,