diff --git a/crates/browser/src/browser_view.rs b/crates/browser/src/browser_view.rs index 9205e50160b499..2f9fe537fed7e4 100644 --- a/crates/browser/src/browser_view.rs +++ b/crates/browser/src/browser_view.rs @@ -30,6 +30,8 @@ use gpui::{ InteractiveElement, IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Subscription, Task, UTF16Selection, WeakEntity, Window, actions, div, point, prelude::*, size, }; +#[cfg(not(target_os = "macos"))] +use gpui::UniformListScrollHandle; use std::ops::Range; use std::sync::atomic::{AtomicBool, Ordering}; use workspace::{ @@ -213,6 +215,8 @@ pub struct BrowserView { #[cfg(not(target_os = "macos"))] sidebar_collapsed: bool, sidebar_visible: bool, + #[cfg(not(target_os = "macos"))] + sidebar_scroll_handle: UniformListScrollHandle, native_sidebar_panel: Option>, focus_listeners_registered: bool, toast_layer: Entity, @@ -309,6 +313,8 @@ impl BrowserView { #[cfg(not(target_os = "macos"))] sidebar_collapsed: false, sidebar_visible: false, + #[cfg(not(target_os = "macos"))] + sidebar_scroll_handle: UniformListScrollHandle::default(), native_sidebar_panel: None, focus_listeners_registered: false, toast_layer, @@ -848,7 +854,9 @@ impl BrowserView { match event { #[cfg(target_os = "macos")] TabEvent::FrameReady => { - cx.notify(); + if self.surface_state == BrowserSurfaceState::Visible { + cx.notify(); + } } TabEvent::NavigateToUrl(url) => { let url = url.clone(); @@ -1493,26 +1501,29 @@ impl Render for BrowserView { .into_any_element(); #[cfg(not(target_os = "macos"))] - let element = match self.tab_bar_mode { - TabBarMode::Horizontal => element - .flex_col() - .child(div().mt(px(-1.)).child(self.render_tab_strip(cx))) - .child(self.bookmark_bar.clone()) - .child(self.render_browser_content(window, cx)) - .into_any_element(), - TabBarMode::Sidebar => element - .flex_row() - .child(self.render_sidebar(cx)) - .child( - div() - .flex_1() - .flex() - .flex_col() - .overflow_hidden() - .child(self.bookmark_bar.clone()) - .child(self.render_browser_content(window, cx)), - ) - .into_any_element(), + let element = { + let pinned_count = self.pinned_tab_count(cx); + match self.tab_bar_mode { + TabBarMode::Horizontal => element + .flex_col() + .child(div().mt(px(-1.)).child(self.render_tab_strip(pinned_count, cx))) + .child(self.bookmark_bar.clone()) + .child(self.render_browser_content(window, cx)) + .into_any_element(), + TabBarMode::Sidebar => element + .flex_row() + .child(self.render_sidebar(pinned_count, cx)) + .child( + div() + .flex_1() + .flex() + .flex_col() + .overflow_hidden() + .child(self.bookmark_bar.clone()) + .child(self.render_browser_content(window, cx)), + ) + .into_any_element(), + } }; div() diff --git a/crates/browser/src/browser_view/tab_strip.rs b/crates/browser/src/browser_view/tab_strip.rs index 5944062cef098a..0f888e204b3992 100644 --- a/crates/browser/src/browser_view/tab_strip.rs +++ b/crates/browser/src/browser_view/tab_strip.rs @@ -4,9 +4,11 @@ use gpui::{ }; #[cfg(not(target_os = "macos"))] use gpui::{ - MouseButton, NativeImageScaling, ParentElement, div, native_image_view, native_tracking_view, - px, rems, + AnyElement, MouseButton, NativeImageScaling, ParentElement, UniformListScrollHandle, div, + native_image_view, native_tracking_view, px, rems, uniform_list, }; +#[cfg(not(target_os = "macos"))] +use std::ops::Range; use ui::prelude::*; use workspace::{Workspace, WorkspaceTabsSidebarKind}; use workspace_chrome::{SidebarNavigationList, SidebarNavigationListItem}; @@ -240,14 +242,256 @@ impl Render for BrowserSidebarPanel { } } +/// Snapshot of tab state needed to build one row in the strip or sidebar. +/// Collected from `Entity` once per render to avoid repeated entity-map lookups. +#[cfg(not(target_os = "macos"))] +struct TabRowData { + title: String, + favicon_url: Option, + is_pinned: bool, +} + impl BrowserView { #[cfg(not(target_os = "macos"))] - pub(super) fn render_tab_strip(&mut self, cx: &mut Context) -> impl IntoElement { + pub(super) fn pinned_tab_count(&self, cx: &mut gpui::Context) -> usize { + self.tabs.iter().filter(|t| t.read(cx).is_pinned()).count() + } + + /// Collect render data for all tabs in one pass (one entity read per tab). + #[cfg(not(target_os = "macos"))] + fn collect_tab_row_data(&self, cx: &gpui::App) -> Vec { + self.tabs + .iter() + .map(|tab| { + let data = tab.read(cx); + TabRowData { + title: data.title().to_string(), + favicon_url: data.favicon_url().map(|s| s.to_string()), + is_pinned: data.is_pinned(), + } + }) + .collect() + } + + /// `uniform_list` processor for the unpinned sidebar tab rows. + /// `range` is 0-based within the unpinned slice; `pinned_count` offsets into `self.tabs`. + #[cfg(not(target_os = "macos"))] + fn render_sidebar_tab_rows( + &mut self, + range: Range, + pinned_count: usize, + cx: &mut Context, + ) -> Vec { let theme = cx.theme(); let active_index = self.active_tab_index; let view = cx.entity().downgrade(); + let tab_radius = cx.theme().component_radius().tab.unwrap_or(px(8.0)); + let btn_radius = cx.theme().component_radius().button.unwrap_or(px(4.0)); + let selected_bg = theme.colors().text.opacity(0.14); + let hover_bg = theme.colors().text.opacity(0.09); + + range + .map(|local_index| { + let tab_index = pinned_count + local_index; + let (title, favicon_url, is_pinned) = { + let data = self.tabs[tab_index].read(cx); + let title = { + let raw = data.title(); + if raw.len() > 24 { + match raw.char_indices().nth(21) { + Some((b, _)) => format!("{}...", &raw[..b]), + None => raw.to_string(), + } + } else { + raw.to_string() + } + }; + let favicon_url = data.favicon_url().map(|s| s.to_string()); + (title, favicon_url, data.is_pinned()) + }; + + let is_active = tab_index == active_index; + let is_hovered = self.hovered_sidebar_tab_index == Some(tab_index); + let is_close_hovered = self.hovered_sidebar_tab_close_index == Some(tab_index); - let pinned_count = self.tabs.iter().filter(|t| t.read(cx).is_pinned()).count(); + let favicon_element = render_tab_favicon( + SharedString::from(format!("sidebar-tab-favicon-{tab_index}")), + favicon_url.as_deref(), + cx, + ); + + let hover_view = view.clone(); + let context_view = view.clone(); + + let tab_content = div() + .id(("sidebar-tab-inner", tab_index)) + .relative() + .flex() + .items_center() + .w(px(SIDEBAR_WIDTH_PX - 8.0)) + .h(px(28.)) + .px_2() + .gap_1() + .flex_shrink_0() + .rounded(tab_radius) + .cursor_pointer() + .when(is_active, |this| this.bg(selected_bg)) + .when(is_hovered && !is_active, |this| this.bg(hover_bg)) + .when(!is_active, |this| { + this.hover(move |style| style.bg(hover_bg)) + }) + .on_click(cx.listener(move |this, _, window, cx| { + this.switch_to_tab(tab_index, window, cx); + })) + .child(favicon_element) + .child( + div() + .flex_1() + .overflow_hidden() + .whitespace_nowrap() + .text_ellipsis() + .text_size(rems(0.75)) + .text_color(if is_active { + theme.colors().text + } else { + theme.colors().text_muted + }) + .child(title), + ) + .when(is_hovered, |this| { + let close_hover_view = view.clone(); + this.child( + div() + .id(SharedString::from(format!( + "sidebar-close-tab-{tab_index}" + ))) + .relative() + .flex() + .items_center() + .justify_center() + .w(px(16.)) + .h(px(16.)) + .rounded(btn_radius) + .cursor_pointer() + .when(is_close_hovered, |this| this.bg(hover_bg)) + .on_click(cx.listener(move |this, _, window, cx| { + this.close_tab_at(tab_index, window, cx); + })) + .child( + native_image_view(SharedString::from(format!( + "sidebar-close-tab-icon-{tab_index}" + ))) + .sf_symbol("xmark") + .w(px(8.)) + .h(px(8.)), + ) + .child( + native_tracking_view(format!( + "sidebar-close-tab-track-{tab_index}" + )) + .on_mouse_enter(move |_, _window, cx| { + close_hover_view + .update(cx, |this, cx| { + if this.hovered_sidebar_tab_close_index + != Some(tab_index) + { + this.hovered_sidebar_tab_close_index = + Some(tab_index); + cx.notify(); + } + }) + .ok(); + }) + .on_mouse_exit({ + let close_hover_view = view.clone(); + move |_, _window, cx| { + close_hover_view + .update(cx, |this, cx| { + if this.hovered_sidebar_tab_close_index + == Some(tab_index) + { + this.hovered_sidebar_tab_close_index = + None; + cx.notify(); + } + }) + .ok(); + } + }) + .absolute() + .top_0() + .left_0() + .size_full(), + ), + ) + }) + .child( + native_tracking_view(format!("sidebar-tab-track-{tab_index}")) + .on_mouse_enter(move |_, _window, cx| { + hover_view + .update(cx, |this, cx| { + if this.hovered_sidebar_tab_index != Some(tab_index) { + this.hovered_sidebar_tab_index = Some(tab_index); + cx.notify(); + } + }) + .ok(); + }) + .on_mouse_exit({ + let hover_view = view.clone(); + move |_, _window, cx| { + hover_view + .update(cx, |this, cx| { + if this.hovered_sidebar_tab_index == Some(tab_index) { + this.hovered_sidebar_tab_index = None; + this.hovered_sidebar_tab_close_index = None; + cx.notify(); + } + }) + .ok(); + } + }) + .absolute() + .top_0() + .left_0() + .size_full(), + ); + + // mb_1 provides the gap between rows (equivalent to gap_1 on the old container) + div() + .w_full() + .mb_1() + .child( + tab_content.on_mouse_down( + MouseButton::Right, + move |event, window, cx| { + show_tab_context_menu( + context_view.clone(), + tab_index, + is_pinned, + event.position, + window, + cx, + ); + }, + ), + ) + .into_any_element() + }) + .collect() + } + + #[cfg(not(target_os = "macos"))] + pub(super) fn render_tab_strip( + &mut self, + pinned_count: usize, + cx: &mut Context, + ) -> impl IntoElement { + let theme = cx.theme(); + let active_index = self.active_tab_index; + let view = cx.entity().downgrade(); + // Pre-collect all tab data in a single pass to avoid repeated entity reads. + let tab_data = self.collect_tab_row_data(cx); h_flex() .w_full() @@ -268,10 +512,9 @@ impl BrowserView { .bg(theme.colors().text.opacity(0.06)) .border_1() .border_color(theme.colors().border.opacity(0.4)) - .children(self.tabs.iter().enumerate().take(pinned_count).map( - |(index, tab)| { - let tab_data = tab.read(cx); - let favicon_url = tab_data.favicon_url(); + .children(tab_data.iter().enumerate().take(pinned_count).map( + |(index, row)| { + let favicon_url = row.favicon_url.as_deref(); let is_active = index == active_index; let is_hovered = self.hovered_top_tab_index == Some(index); let selected_bg = theme.colors().text.opacity(0.14); @@ -358,17 +601,16 @@ impl BrowserView { )), ) }) - // Unpinned tabs + // Unpinned tabs — iterate pre-collected data, no further entity reads needed .children( - self.tabs + tab_data .iter() .enumerate() .skip(pinned_count) - .map(|(index, tab)| { - let tab_data = tab.read(cx); - let title = tab_data.title().to_string(); - let favicon_url = tab_data.favicon_url(); - let is_pinned = tab_data.is_pinned(); + .map(|(index, row)| { + let title = row.title.clone(); + let favicon_url = row.favicon_url.as_deref(); + let is_pinned = row.is_pinned; let is_active = index == active_index; let is_hovered = self.hovered_top_tab_index == Some(index); let is_close_hovered = self.hovered_top_tab_close_index == Some(index); @@ -613,13 +855,26 @@ impl BrowserView { } #[cfg(not(target_os = "macos"))] - pub(super) fn render_sidebar(&mut self, cx: &mut Context) -> impl IntoElement { + pub(super) fn render_sidebar( + &mut self, + pinned_count: usize, + cx: &mut Context, + ) -> impl IntoElement { let theme = cx.theme(); - let active_index = self.active_tab_index; - let view = cx.entity().downgrade(); + let unpinned_count = self.tabs.len().saturating_sub(pinned_count); + // Clone the handle so the uniform_list element can track scroll state without + // borrowing `self` for the lifetime of the returned element. + let scroll_handle = self.sidebar_scroll_handle.clone(); - let pinned_count = self.tabs.iter().filter(|t| t.read(cx).is_pinned()).count(); - let grid_cols = pinned_count.min(3); + // Pre-collect pinned tab data in a single pass (avoids repeated entity reads in the grid). + let pinned_data = if pinned_count > 0 { + self.collect_tab_row_data(cx) + .into_iter() + .take(pinned_count) + .collect::>() + } else { + Vec::new() + }; v_flex() .h_full() @@ -629,246 +884,95 @@ impl BrowserView { .bg(theme.colors().title_bar_background) .border_r_1() .border_color(theme.colors().border) - .child( - v_flex() - .id("sidebar-tab-list") - .flex_1() - .items_stretch() - .overflow_y_scroll() - .p_1() - .gap_1() - .when(pinned_count > 0, |this| { - let pinned_rows: Vec> = (0..pinned_count) - .collect::>() - .chunks(grid_cols) - .map(|chunk| chunk.to_vec()) - .collect(); + // ── Pinned tab grid ── fixed at top, never scrolls ───────────── + .when(pinned_count > 0, |outer| { + let grid_cols = pinned_count.min(3); + let active_index = self.active_tab_index; + let view = cx.entity().downgrade(); + let tab_radius = cx.theme().component_radius().tab.unwrap_or(px(8.0)); + let selected_bg = theme.colors().text.opacity(0.14); + let hover_bg = theme.colors().text.opacity(0.09); - this.child( - v_flex().gap_1().children( - pinned_rows.into_iter().map(|row| { - h_flex() - .gap_1() - .children(row.into_iter().map(|index| { - let tab = &self.tabs[index]; - let tab_data = tab.read(cx); - let favicon_url = tab_data.favicon_url(); - let is_active = index == active_index; - let is_hovered = - self.hovered_sidebar_tab_index == Some(index); - let selected_bg = - theme.colors().text.opacity(0.14); - let hover_bg = theme.colors().text.opacity(0.09); - - let favicon_element = render_tab_favicon( - SharedString::from(format!( - "sidebar-tab-favicon-{index}" - )), - favicon_url, - cx, - ); - - let hover_view = view.clone(); - let context_view = view.clone(); - - div() - .id(("sidebar-tab-inner", index)) - .relative() - .flex() - .flex_1() - .items_center() - .justify_center() - .h(px(36.)) - .flex_shrink_0() - .rounded(cx.theme().component_radius().tab.unwrap_or(px(8.0))) - .cursor_pointer() - .when(is_active, |this| { - this.bg(selected_bg) - }) - .when(is_hovered && !is_active, |this| { - this.bg(hover_bg) - }) - .when(!is_active, |this| { - this.hover(move |style| { - style.bg(hover_bg) - }) - }) - .on_click(cx.listener( - move |this, _, window, cx| { - this.switch_to_tab( - index, window, cx, - ); - }, - )) - .on_mouse_down( - MouseButton::Right, - move |event, window, cx| { - show_tab_context_menu( - context_view.clone(), - index, - true, - event.position, - window, - cx, - ); - }, - ) - .child(favicon_element) - .child( - native_tracking_view(format!( - "sidebar-tab-track-{index}" - )) - .on_mouse_enter( - move |_, _window, cx| { - hover_view - .update(cx, |this, cx| { - if this - .hovered_sidebar_tab_index - != Some(index) - { - this.hovered_sidebar_tab_index = Some(index); - cx.notify(); - } - }) - .ok(); - }, - ) - .on_mouse_exit({ - let hover_view = view.clone(); - move |_, _window, cx| { - hover_view - .update(cx, |this, cx| { - if this - .hovered_sidebar_tab_index - == Some(index) - { - this.hovered_sidebar_tab_index = None; - this.hovered_sidebar_tab_close_index = None; - cx.notify(); - } - }) - .ok(); - } - }) - .absolute() - .top_0() - .left_0() - .size_full(), - ) - .into_any_element() - })) - .into_any_element() - }), - ), - ) - }) - .children(self.tabs.iter().enumerate().skip(pinned_count).map( - |(index, tab)| { - let tab_data = tab.read(cx); - let title = tab_data.title().to_string(); - let favicon_url = tab_data.favicon_url(); - let is_pinned = tab_data.is_pinned(); - let is_active = index == active_index; - let is_hovered = self.hovered_sidebar_tab_index == Some(index); - let is_close_hovered = - self.hovered_sidebar_tab_close_index == Some(index); - let selected_bg = theme.colors().text.opacity(0.14); - let hover_bg = theme.colors().text.opacity(0.09); - - let favicon_element = render_tab_favicon( - SharedString::from(format!("sidebar-tab-favicon-{index}")), - favicon_url, - cx, - ); + let pinned_rows: Vec> = (0..pinned_count) + .collect::>() + .chunks(grid_cols) + .map(|c| c.to_vec()) + .collect(); - let display_title = if title.len() > 24 { - let truncated = match title.char_indices().nth(21) { - Some((byte_index, _)) => &title[..byte_index], - None => &title, - }; - format!("{truncated}...") - } else { - title - }; + outer.child( + v_flex() + .p_1() + .gap_1() + .child(v_flex().gap_1().children( + pinned_rows.into_iter().map(|row| { + let row_view = view.clone(); + h_flex() + .gap_1() + .children(row.into_iter().map(|index| { + let row_data = &pinned_data[index]; + let favicon_url = row_data.favicon_url.as_deref(); + let is_active = index == active_index; + let is_hovered = + self.hovered_sidebar_tab_index == Some(index); + + let favicon_element = render_tab_favicon( + SharedString::from(format!( + "sidebar-tab-favicon-{index}" + )), + favicon_url, + cx, + ); + let hover_view = row_view.clone(); + let context_view = row_view.clone(); - let hover_view = view.clone(); - let context_view = view.clone(); - let tab_content = div() - .id(("sidebar-tab-inner", index)) - .relative() - .flex() - .items_center() - .w(px(SIDEBAR_WIDTH_PX - 8.0)) - .h(px(28.)) - .px_2() - .gap_1() - .flex_shrink_0() - .rounded(cx.theme().component_radius().tab.unwrap_or(px(8.0))) - .cursor_pointer() - .when(is_active, |this| this.bg(selected_bg)) - .when(is_hovered && !is_active, |this| this.bg(hover_bg)) - .when(!is_active, |this| { - this.hover(move |style| style.bg(hover_bg)) - }) - .on_click(cx.listener(move |this, _, window, cx| { - this.switch_to_tab(index, window, cx); - })) - .child(favicon_element) - .child( - div() - .flex_1() - .overflow_hidden() - .whitespace_nowrap() - .text_ellipsis() - .text_size(rems(0.75)) - .text_color(if is_active { - theme.colors().text - } else { - theme.colors().text_muted - }) - .child(display_title), - ) - .when(is_hovered, |this| { - let close_hover_view = view.clone(); - this.child( div() - .id(SharedString::from(format!( - "sidebar-close-tab-{index}" - ))) + .id(("sidebar-tab-inner", index)) .relative() .flex() + .flex_1() .items_center() .justify_center() - .w(px(16.)) - .h(px(16.)) - .rounded(cx.theme().component_radius().button.unwrap_or(px(4.0))) + .h(px(36.)) + .flex_shrink_0() + .rounded(tab_radius) .cursor_pointer() - .when(is_close_hovered, |this| this.bg(hover_bg)) + .when(is_active, |this| this.bg(selected_bg)) + .when(is_hovered && !is_active, |this| { + this.bg(hover_bg) + }) + .when(!is_active, |this| { + this.hover(move |style| style.bg(hover_bg)) + }) .on_click(cx.listener( move |this, _, window, cx| { - this.close_tab_at(index, window, cx); + this.switch_to_tab(index, window, cx); }, )) - .child( - native_image_view(SharedString::from(format!( - "sidebar-close-tab-icon-{index}" - ))) - .sf_symbol("xmark") - .w(px(8.)) - .h(px(8.)), + .on_mouse_down( + MouseButton::Right, + move |event, window, cx| { + show_tab_context_menu( + context_view.clone(), + index, + true, + event.position, + window, + cx, + ); + }, ) + .child(favicon_element) .child( native_tracking_view(format!( - "sidebar-close-tab-track-{index}" + "sidebar-tab-track-{index}" )) .on_mouse_enter(move |_, _window, cx| { - close_hover_view + hover_view .update(cx, |this, cx| { - if this - .hovered_sidebar_tab_close_index + if this.hovered_sidebar_tab_index != Some(index) { - this.hovered_sidebar_tab_close_index = + this.hovered_sidebar_tab_index = Some(index); cx.notify(); } @@ -876,14 +980,15 @@ impl BrowserView { .ok(); }) .on_mouse_exit({ - let close_hover_view = view.clone(); + let hover_view = row_view.clone(); move |_, _window, cx| { - close_hover_view + hover_view .update(cx, |this, cx| { - if this - .hovered_sidebar_tab_close_index + if this.hovered_sidebar_tab_index == Some(index) { + this.hovered_sidebar_tab_index = + None; this.hovered_sidebar_tab_close_index = None; cx.notify(); @@ -896,66 +1001,35 @@ impl BrowserView { .top_0() .left_0() .size_full(), - ), - ) - }) - .child( - native_tracking_view(format!("sidebar-tab-track-{index}")) - .on_mouse_enter(move |_, _window, cx| { - hover_view - .update(cx, |this, cx| { - if this.hovered_sidebar_tab_index - != Some(index) - { - this.hovered_sidebar_tab_index = - Some(index); - cx.notify(); - } - }) - .ok(); - }) - .on_mouse_exit({ - let hover_view = view.clone(); - move |_, _window, cx| { - hover_view - .update(cx, |this, cx| { - if this.hovered_sidebar_tab_index - == Some(index) - { - this.hovered_sidebar_tab_index = None; - this.hovered_sidebar_tab_close_index = - None; - cx.notify(); - } - }) - .ok(); - } - }) - .absolute() - .top_0() - .left_0() - .size_full(), - ); - - div().w_full().child( - tab_content.on_mouse_down( - MouseButton::Right, - move |event, window, cx| { - show_tab_context_menu( - context_view.clone(), - index, - is_pinned, - event.position, - window, - cx, - ); - }, - ), - ) - }, - )), - ) + ) + .into_any_element() + })) + .into_any_element() + }), + )), + ) + }) + // ── Virtualized unpinned tab list ─────────────────────────────── + // uniform_list renders only the rows visible in the viewport; + // with 50+ tabs this drops element construction from O(n) to O(visible). .child( + uniform_list( + "sidebar-unpinned-tabs", + unpinned_count, + { + let pinned_count = pinned_count; + cx.processor(move |this, range: Range, _window, cx| { + this.render_sidebar_tab_rows(range, pinned_count, cx) + }) + }, + ) + .p_1() + .flex_grow() + .track_scroll(&scroll_handle), + ) + // ── New tab button pinned at bottom ───────────────────────────── + .child({ + let view = cx.entity().downgrade(); div() .w_full() .p_1() @@ -1000,7 +1074,6 @@ impl BrowserView { } }) .on_mouse_exit({ - let view = view.clone(); move |_, _window, cx| { view.update(cx, |this, cx| { if this.hovered_sidebar_new_tab_button { @@ -1016,7 +1089,7 @@ impl BrowserView { .left_0() .size_full(), ), - ), - ) + ) + }) } } diff --git a/crates/browser/src/client.rs b/crates/browser/src/client.rs index 1efdfcb00202a3..5084132340941d 100644 --- a/crates/browser/src/client.rs +++ b/crates/browser/src/client.rs @@ -24,6 +24,8 @@ use crate::permission_handler::{OsrPermissionHandler, PermissionHandlerBuilder}; use crate::render_handler::{OsrRenderHandler, RenderHandlerBuilder, RenderState}; use crate::request_handler::{OsrRequestHandler, RequestHandlerBuilder}; use crate::text_input::extract_text_input_state_from_message; +#[cfg(target_os = "macos")] +use core_video::pixel_buffer::CVPixelBuffer; use parking_lot::Mutex; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -189,16 +191,29 @@ wrap_client! { } impl ClientBuilder { - pub fn build(render_state: Arc>, event_sender: EventSender) -> cef::Client { - Self::build_inner(render_state, event_sender, KeyboardHandlerBuilder::build()) + pub fn build( + render_state: Arc>, + #[cfg(target_os = "macos")] current_frame: Arc>>, + event_sender: EventSender, + ) -> cef::Client { + Self::build_inner( + render_state, + #[cfg(target_os = "macos")] + current_frame, + event_sender, + KeyboardHandlerBuilder::build(), + ) } pub fn build_for_popup( render_state: Arc>, + #[cfg(target_os = "macos")] current_frame: Arc>>, event_sender: EventSender, ) -> cef::Client { Self::build_inner( render_state, + #[cfg(target_os = "macos")] + current_frame, event_sender, PopupKeyboardHandlerBuilder::build(), ) @@ -206,10 +221,16 @@ impl ClientBuilder { fn build_inner( render_state: Arc>, + #[cfg(target_os = "macos")] current_frame: Arc>>, event_sender: EventSender, keyboard_handler: cef::KeyboardHandler, ) -> cef::Client { - let render_handler = OsrRenderHandler::new(render_state, event_sender.clone()); + let render_handler = OsrRenderHandler::new( + render_state, + #[cfg(target_os = "macos")] + current_frame, + event_sender.clone(), + ); let load_handler = OsrLoadHandler::new(event_sender.clone()); let display_handler = OsrDisplayHandler::new(event_sender.clone()); let life_span_handler = OsrLifeSpanHandler::new(event_sender.clone()); diff --git a/crates/browser/src/life_span_handler.rs b/crates/browser/src/life_span_handler.rs index 4d8170a49d2a0c..3ec36a4611dfe5 100644 --- a/crates/browser/src/life_span_handler.rs +++ b/crates/browser/src/life_span_handler.rs @@ -11,6 +11,8 @@ use cef::{ Browser, ImplLifeSpanHandler, LifeSpanHandler, WrapLifeSpanHandler, rc::Rc as _, wrap_life_span_handler, }; +#[cfg(target_os = "macos")] +use core_video::pixel_buffer::CVPixelBuffer; use parking_lot::Mutex; use std::sync::Arc; @@ -26,8 +28,15 @@ impl OsrLifeSpanHandler { fn popup_client() -> cef::Client { let render_state = Arc::new(Mutex::new(RenderState::default())); + #[cfg(target_os = "macos")] + let current_frame: Arc>> = Arc::new(Mutex::new(None)); let (popup_sender, _popup_receiver) = crate::events::event_channel(); - ClientBuilder::build_for_popup(render_state, popup_sender) + ClientBuilder::build_for_popup( + render_state, + #[cfg(target_os = "macos")] + current_frame, + popup_sender, + ) } } diff --git a/crates/browser/src/render_handler.rs b/crates/browser/src/render_handler.rs index d677ba8a3e0d94..abb4fcf1d09122 100644 --- a/crates/browser/src/render_handler.rs +++ b/crates/browser/src/render_handler.rs @@ -20,12 +20,12 @@ use io_surface::IOSurface; use parking_lot::Mutex; use std::sync::Arc; +/// Viewport geometry shared between the GPUI resize path and CEF's view_rect/screen_info callbacks. +/// Kept separate from the frame buffer so geometry reads never contend with 60fps frame writes. pub struct RenderState { pub width: u32, pub height: u32, pub scale_factor: f32, - #[cfg(target_os = "macos")] - pub current_frame: Option, } impl Default for RenderState { @@ -34,8 +34,6 @@ impl Default for RenderState { width: 800, height: 600, scale_factor: 1.0, - #[cfg(target_os = "macos")] - current_frame: None, } } } @@ -44,14 +42,24 @@ impl Default for RenderState { pub struct OsrRenderHandler { state: Arc>, #[cfg(target_os = "macos")] + current_frame: Arc>>, + #[cfg(target_os = "macos")] sender: EventSender, } impl OsrRenderHandler { - pub fn new(state: Arc>, sender: EventSender) -> Self { + pub fn new( + state: Arc>, + #[cfg(target_os = "macos")] current_frame: Arc>>, + sender: EventSender, + ) -> Self { #[cfg(target_os = "macos")] { - Self { state, sender } + Self { + state, + current_frame, + sender, + } } #[cfg(not(target_os = "macos"))] @@ -153,7 +161,7 @@ wrap_render_handler! { } }; - self.handler.state.lock().current_frame = Some(pixel_buffer); + *self.handler.current_frame.lock() = Some(pixel_buffer); let _ = self.handler.sender.send(BrowserEvent::FrameReady); } diff --git a/crates/browser/src/tab.rs b/crates/browser/src/tab.rs index df1d1a0da0e531..0e2b8f2d000651 100644 --- a/crates/browser/src/tab.rs +++ b/crates/browser/src/tab.rs @@ -105,6 +105,8 @@ pub struct BrowserTab { browser_id: Option, client: cef::Client, render_state: Arc>, + #[cfg(target_os = "macos")] + current_frame: Arc>>, event_receiver: EventReceiver, url: String, title: String, @@ -127,13 +129,22 @@ impl EventEmitter for BrowserTab {} impl BrowserTab { pub fn new(_cx: &mut Context) -> Self { let render_state = Arc::new(Mutex::new(RenderState::default())); + #[cfg(target_os = "macos")] + let current_frame: Arc>> = Arc::new(Mutex::new(None)); let (sender, receiver) = events::event_channel(); - let client = ClientBuilder::build(render_state.clone(), sender); + let client = ClientBuilder::build( + render_state.clone(), + #[cfg(target_os = "macos")] + current_frame.clone(), + sender, + ); Self { browser_id: None, client, render_state, + #[cfg(target_os = "macos")] + current_frame, event_receiver: receiver, url: String::from("glass://newtab"), title: String::from("New Tab"), @@ -160,13 +171,22 @@ impl BrowserTab { _cx: &mut Context, ) -> Self { let render_state = Arc::new(Mutex::new(RenderState::default())); + #[cfg(target_os = "macos")] + let current_frame: Arc>> = Arc::new(Mutex::new(None)); let (sender, receiver) = events::event_channel(); - let client = ClientBuilder::build(render_state.clone(), sender); + let client = ClientBuilder::build( + render_state.clone(), + #[cfg(target_os = "macos")] + current_frame.clone(), + sender, + ); Self { browser_id: None, client, render_state, + #[cfg(target_os = "macos")] + current_frame, event_receiver: receiver, url, title, @@ -225,7 +245,9 @@ impl BrowserTab { } #[cfg(target_os = "macos")] BrowserEvent::FrameReady => { - cx.emit(TabEvent::FrameReady); + if !is_suspended { + cx.emit(TabEvent::FrameReady); + } } BrowserEvent::BrowserCreated => {} BrowserEvent::LoadError { url, error_text } => { @@ -672,7 +694,7 @@ impl BrowserTab { #[cfg(target_os = "macos")] pub fn current_frame(&self) -> Option { - self.render_state.lock().current_frame.clone() + self.current_frame.lock().clone() } pub fn url(&self) -> &str { @@ -768,7 +790,7 @@ impl BrowserTab { } #[cfg(target_os = "macos")] { - self.render_state.lock().current_frame = None; + *self.current_frame.lock() = None; } }