diff --git a/crates/op-editor-ui/src/widgets/icons.rs b/crates/op-editor-ui/src/widgets/icons.rs index d65972e68..abacc9519 100644 --- a/crates/op-editor-ui/src/widgets/icons.rs +++ b/crates/op-editor-ui/src/widgets/icons.rs @@ -74,6 +74,8 @@ pub enum Icon { PanelLeft, /// FolderOpen — TopBar folder. FolderOpen, + /// GitBranch — TopBar git-panel toggle next to the file name. + GitBranch, /// Sparkles — agent active indicator. Sparkles, /// X — close affordance. @@ -257,6 +259,7 @@ impl Icon { Icon::Hash => HASH, Icon::PanelLeft => PANEL_LEFT, Icon::FolderOpen => FOLDER_OPEN, + Icon::GitBranch => GIT_BRANCH, Icon::Sparkles => SPARKLES, Icon::Close => CLOSE, Icon::Trash => TRASH, @@ -402,6 +405,7 @@ impl Icon { "download" => Icon::Download, "file-text" => Icon::FileText, "folder-open" | "folder" => Icon::FolderOpen, + "git-branch" | "git" => Icon::GitBranch, "sparkles" => Icon::Sparkles, "diamond" => Icon::Diamond, "component" => Icon::Component, diff --git a/crates/op-editor-ui/src/widgets/icons_data.rs b/crates/op-editor-ui/src/widgets/icons_data.rs index d172b1311..19fca41ee 100644 --- a/crates/op-editor-ui/src/widgets/icons_data.rs +++ b/crates/op-editor-ui/src/widgets/icons_data.rs @@ -122,6 +122,14 @@ pub(super) const FOLDER_OPEN: &[&str] = &[ "m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2", ]; +pub(super) const GIT_BRANCH: &[&str] = &[ + // lucide git-branch: line + two r=3 circles + connecting arc. + "M6 3v12", + "M18 6 m-3 0 a3 3 0 1 0 6 0 a3 3 0 1 0 -6 0", + "M6 18 m-3 0 a3 3 0 1 0 6 0 a3 3 0 1 0 -6 0", + "M18 9a9 9 0 0 1-9 9", +]; + pub(super) const SPARKLES: &[&str] = &[ "M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z", "M20 2v4", diff --git a/crates/op-editor-ui/src/widgets/icons_tests.rs b/crates/op-editor-ui/src/widgets/icons_tests.rs index f54694214..1a6bb5cf9 100644 --- a/crates/op-editor-ui/src/widgets/icons_tests.rs +++ b/crates/op-editor-ui/src/widgets/icons_tests.rs @@ -85,6 +85,7 @@ fn every_variant_paints_at_least_one_primitive() { Icon::Hash, Icon::PanelLeft, Icon::FolderOpen, + Icon::GitBranch, Icon::Sparkles, Icon::Close, Icon::ChevronUp, diff --git a/crates/op-editor-ui/src/widgets/top_bar.rs b/crates/op-editor-ui/src/widgets/top_bar.rs index 93c0190f1..9d0664e17 100644 --- a/crates/op-editor-ui/src/widgets/top_bar.rs +++ b/crates/op-editor-ui/src/widgets/top_bar.rs @@ -79,6 +79,8 @@ pub enum TopBarHit { ToggleLocale, /// Agents and MCP chip — open the agent settings modal. OpenAgentSettings, + /// Git-branch button next to the file name — toggle the git panel. + ToggleGitPanel, } pub struct TopBar { @@ -108,6 +110,10 @@ pub struct TopBar { /// Dark mode (click to go light), a Moon glyph in Light mode /// (click to go dark). pub theme_mode: op_editor_core::ThemeMode, + /// Current git branch when the open document is in a repo — shown + /// beside the file name. `None` = no branch (the button still + /// paints, icon-only, as a toggle for the git panel). + pub git_branch: Option, } impl TopBar { @@ -125,6 +131,7 @@ impl TopBar { traffic_hover: false, fullscreen: false, theme_mode: op_editor_core::ThemeMode::Dark, + git_branch: None, } } @@ -161,6 +168,7 @@ impl TopBar { traffic_hover: ui.topbar_traffic_hover, fullscreen: ui.window_fullscreen, theme_mode: ui.theme_mode, + git_branch: ui.git_panel.branch.clone(), } } @@ -263,6 +271,34 @@ impl TopBar { } } + /// Git-panel toggle button — sits just right of the centred file + /// name. Width holds the branch glyph plus an optional branch + /// label. Shared by paint + hit-test so they can't drift. + fn git_button_rect(&self, top_bar_rect: Rect) -> Rect { + let center_y = top_bar_rect.origin.y + top_bar_rect.size.y / 2.0; + // The name is *centred* using the 9 px/char heuristic, but a + // CJK glyph renders ~14 px wide, so the real right edge is + // further out — use a CJK-aware estimate so the button clears + // the (often CJK) file name instead of overlapping it. + let center_approx = self.file_name.chars().count() as f32 * 9.0; + let render_w: f32 = self + .file_name + .chars() + .map(|c| if is_wide_glyph(c) { 14.0 } else { 7.5 }) + .sum(); + let filename_left = top_bar_rect.origin.x + (top_bar_rect.size.x - center_approx) / 2.0; + let filename_right = filename_left + render_w; + let branch_w = self + .git_branch + .as_deref() + .map(|b| 6.0 + b.chars().count() as f32 * 7.0) + .unwrap_or(0.0); + Rect { + origin: Point2D::new(filename_right + 10.0, center_y - ICON_BUTTON / 2.0), + size: Point2D::new(ICON_SIZE + 8.0 + branch_w, ICON_BUTTON), + } + } + /// Resolve a press on the left-edge window-control dots. /// `None` for a press anywhere else (including the app's own /// buttons). The desktop runner consults this before its normal @@ -334,6 +370,10 @@ impl TopBar { if rect_contains(figma_rect, point) { return Some(TopBarHit::OpenFigmaImport); } + // Git-panel toggle, just right of the centred file name. + if rect_contains(self.git_button_rect(rect), point) { + return Some(TopBarHit::ToggleGitPanel); + } // Right cluster: Maximize / Sun / Globe-with-chevron (right→left). // Maximize + Sun are normal ICON_BUTTON wide; Globe is // GLOBE_BUTTON_WIDTH wide because it carries a chevron. @@ -546,6 +586,33 @@ impl Widget for TopBar { ), ); + // Git-panel button just right of the file name (TS GitButton): + // a branch glyph + optional branch name. Always shown — a + // click toggles the git panel (which offers `init` when the + // doc isn't yet in a repo). + let git_rect = self.git_button_rect(rect); + draw_icon( + cx.backend, + Icon::GitBranch, + Point2D::new(git_rect.origin.x, glyph_top(center_y, ICON_SIZE)), + ICON_SIZE, + self.theme.muted_foreground, + 1.4, + ); + if let Some(branch) = self.git_branch.as_deref() { + let label = TextLayout::single_run( + branch, + "system-ui", + 11.0, + to_jian_color(self.theme.muted_foreground), + Point2D::new(0.0, 0.0), + ); + cx.backend.draw_text( + &label, + Point2D::new(git_rect.origin.x + ICON_SIZE + 6.0, center_y + 4.0), + ); + } + // ── Right cluster ────────────────────────────────────── // Right → left: Maximize | Sun | Globe+Chevron. Globe is a // wider compound button (signals the dropdown affordance). @@ -741,6 +808,14 @@ fn paint_figma_button(cx: &mut PaintCx<'_>, theme: &Theme, x: f32, center_y: f32 ); } +/// Rough "is this a full-width (CJK/Hangul/full-width-form) glyph" +/// test — used only to estimate the rendered file-name width so the +/// git button clears it. +fn is_wide_glyph(c: char) -> bool { + let cp = c as u32; + matches!(cp, 0x1100..=0x11FF | 0x2E80..=0x9FFF | 0xAC00..=0xD7AF | 0xF900..=0xFAFF | 0xFF00..=0xFFEF) +} + /// Top-left y for a glyph of `size` vertically centred on `center_y`. /// Every top-bar glyph routes through this so the whole bar shares /// one center line. diff --git a/crates/op-host-native/src/widget_host/press.rs b/crates/op-host-native/src/widget_host/press.rs index 684518388..79cd02493 100644 --- a/crates/op-host-native/src/widget_host/press.rs +++ b/crates/op-host-native/src/widget_host/press.rs @@ -332,6 +332,25 @@ impl WidgetHostNative { self.mark_dirty(); return true; } + TopBarHit::ToggleGitPanel => { + // Mirror `main.rs` A::ToggleGitPanel bookkeeping; the + // binary's per-frame `if git_panel.open { refresh }` + // performs the actual repo scan. + let panel = &mut self.editor_state.editor_ui.git_panel; + let opening = !panel.open; + panel.open = opening; + if opening { + panel.loading = true; + } else { + panel.commit_focused = false; + panel.remote_focused = false; + panel.https_focused = false; + panel.diff = None; + panel.merge_resolve = None; + } + self.mark_dirty(); + return true; + } } } if rect_contains(top_bar_rect, Point2D::new(x, y)) { diff --git a/crates/op-host-web/src/widget_host/press.rs b/crates/op-host-web/src/widget_host/press.rs index daff46697..0512bb4fe 100644 --- a/crates/op-host-web/src/widget_host/press.rs +++ b/crates/op-host-web/src/widget_host/press.rs @@ -261,6 +261,9 @@ impl WidgetHost { TopBarHit::OpenFigmaImport => { self.editor_state.editor_ui.figma_import_open = true; } + TopBarHit::ToggleGitPanel => { + self.editor_state.editor_ui.git_panel.open ^= true; + } } self.mark_dirty(); return true;