use std::sync::Arc; use gpui::{ div, prelude::FluentBuilder, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, WeakEntity, Window, }; use rust_i18n::t; use crate::{ button::{Button, ButtonVariants as _}, dock::PanelInfo, h_flex, popup_menu::{PopupMenu, PopupMenuExt}, tab::{Tab, TabBar}, v_flex, ActiveTheme, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt as _, }; use super::{ ClosePanel, DockArea, DockPlacement, Panel, PanelControl, PanelEvent, PanelState, PanelStyle, PanelView, StackPanel, ToggleZoom, }; #[derive(Clone)] struct TabState { closable: bool, zoomable: Option, draggable: bool, droppable: bool, active_panel: Option>, } #[derive(Clone)] pub(crate) struct DragPanel { pub(crate) panel: Arc, pub(crate) tab_panel: Entity, } impl DragPanel { pub(crate) fn new(panel: Arc, tab_panel: Entity) -> Self { Self { panel, tab_panel } } } impl Render for DragPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .id("drag-panel") .cursor_grab() .py_1() .px_3() .w_24() .overflow_hidden() .whitespace_nowrap() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .text_color(cx.theme().tab_foreground) .bg(cx.theme().tab_active) .opacity(0.75) .child(self.panel.title(window, cx)) } } pub struct TabPanel { focus_handle: FocusHandle, dock_area: WeakEntity, /// The stock_panel can be None, if is None, that means the panels can't be split or move stack_panel: Option>, pub(crate) panels: Vec>, pub(crate) active_ix: usize, /// If this is true, the Panel closable will follow the active panel's closable, /// otherwise this TabPanel will not able to close pub(crate) closable: bool, tab_bar_scroll_handle: ScrollHandle, is_zoomed: bool, is_collapsed: bool, /// When drag move, will get the placement of the panel to be split will_split_placement: Option, } impl Panel for TabPanel { fn panel_name(&self) -> &'static str { "TabPanel" } fn title(&self, window: &Window, cx: &App) -> gpui::AnyElement { self.active_panel(cx) .map(|panel| panel.title(window, cx)) .unwrap_or("Empty Tab".into_any_element()) } fn closable(&self, cx: &App) -> bool { if !self.closable { return false; } self.active_panel(cx) .map(|panel| panel.closable(cx)) .unwrap_or(false) } fn zoomable(&self, cx: &App) -> Option { self.active_panel(cx).and_then(|panel| panel.zoomable(cx)) } fn visible(&self, cx: &App) -> bool { self.visible_panels(cx).next().is_some() } fn popup_menu(&self, menu: PopupMenu, window: &Window, cx: &App) -> PopupMenu { if let Some(panel) = self.active_panel(cx) { panel.popup_menu(menu, window, cx) } else { menu } } fn toolbar_buttons(&self, window: &mut Window, cx: &mut App) -> Option> { self.active_panel(cx) .and_then(|panel| panel.toolbar_buttons(window, cx)) } fn dump(&self, cx: &App) -> PanelState { let mut state = PanelState::new(self); for panel in self.panels.iter() { state.add_child(panel.dump(cx)); state.info = PanelInfo::tabs(self.active_ix); } state } } impl TabPanel { pub fn new( stack_panel: Option>, dock_area: WeakEntity, _: &mut Window, cx: &mut Context, ) -> Self { Self { focus_handle: cx.focus_handle(), dock_area, stack_panel, panels: Vec::new(), active_ix: 0, tab_bar_scroll_handle: ScrollHandle::new(), will_split_placement: None, is_zoomed: false, is_collapsed: false, closable: true, } } pub(super) fn set_parent(&mut self, view: WeakEntity) { self.stack_panel = Some(view); } /// Return current active_panel View pub fn active_panel(&self, cx: &App) -> Option> { let panel = self.panels.get(self.active_ix); if let Some(panel) = panel { if panel.visible(cx) { Some(panel.clone()) } else { // Return the first visible panel self.visible_panels(cx).next() } } else { None } } fn set_active_ix(&mut self, ix: usize, window: &mut Window, cx: &mut Context) { if ix == self.active_ix { return; } let last_active_ix = self.active_ix; self.active_ix = ix; self.tab_bar_scroll_handle.scroll_to_item(ix); self.focus_active_panel(window, cx); // Sync the active state to all panels cx.spawn_in(window, |view, mut cx| async move { _ = cx.update(|window, cx| { _ = view.update(cx, |view, cx| { if let Some(last_active) = view.panels.get(last_active_ix) { last_active.set_active(false, window, cx); } if let Some(active) = view.panels.get(view.active_ix) { active.set_active(true, window, cx); } }); }); }) .detach(); cx.emit(PanelEvent::LayoutChanged); cx.notify(); } /// Add a panel to the end of the tabs pub fn add_panel( &mut self, panel: Arc, window: &mut Window, cx: &mut Context, ) { self.add_panel_with_active(panel, true, window, cx); } fn add_panel_with_active( &mut self, panel: Arc, active: bool, window: &mut Window, cx: &mut Context, ) { assert_ne!( panel.panel_name(cx), "StackPanel", "can not allows add `StackPanel` to `TabPanel`" ); if self .panels .iter() .any(|p| p.view().entity_id() == panel.view().entity_id()) { return; } self.panels.push(panel); // set the active panel to the new panel if active { self.set_active_ix(self.panels.len() - 1, window, cx); } cx.emit(PanelEvent::LayoutChanged); cx.notify(); } /// Add panel to try to split pub fn add_panel_at( &mut self, panel: Arc, placement: Placement, size: Option, window: &mut Window, cx: &mut Context, ) { cx.spawn_in(window, |view, mut cx| async move { cx.update(|window, cx| { view.update(cx, |view, cx| { view.will_split_placement = Some(placement); view.split_panel(panel, placement, size, window, cx) }) .ok() }) .ok() }) .detach(); cx.emit(PanelEvent::LayoutChanged); cx.notify(); } fn insert_panel_at( &mut self, panel: Arc, ix: usize, window: &mut Window, cx: &mut Context, ) { if self .panels .iter() .any(|p| p.view().entity_id() == panel.view().entity_id()) { return; } self.panels.insert(ix, panel); self.set_active_ix(ix, window, cx); cx.emit(PanelEvent::LayoutChanged); cx.notify(); } /// Remove a panel from the tab panel pub fn remove_panel( &mut self, panel: Arc, window: &mut Window, cx: &mut Context, ) { self.detach_panel(panel, window, cx); self.remove_self_if_empty(window, cx); cx.emit(PanelEvent::ZoomOut); cx.emit(PanelEvent::LayoutChanged); } fn detach_panel( &mut self, panel: Arc, window: &mut Window, cx: &mut Context, ) { let panel_view = panel.view(); self.panels.retain(|p| p.view() != panel_view); if self.active_ix >= self.panels.len() { self.set_active_ix(self.panels.len().saturating_sub(1), window, cx) } } /// Check to remove self from the parent StackPanel, if there is no panel left fn remove_self_if_empty(&self, window: &mut Window, cx: &mut Context) { if !self.panels.is_empty() { return; } let tab_view = cx.entity().clone(); if let Some(stack_panel) = self.stack_panel.as_ref() { _ = stack_panel.update(cx, |view, cx| { view.remove_panel(Arc::new(tab_view), window, cx); }); } } pub(super) fn set_collapsed( &mut self, collapsed: bool, _: &mut Window, cx: &mut Context, ) { self.is_collapsed = collapsed; cx.notify(); } fn is_locked(&self, cx: &App) -> bool { let Some(dock_area) = self.dock_area.upgrade() else { return true; }; if dock_area.read(cx).is_locked() { return true; } if self.is_zoomed { return true; } self.stack_panel.is_none() } /// Return true if self or parent only have last panel. fn is_last_panel(&self, cx: &App) -> bool { if let Some(parent) = &self.stack_panel { if let Some(stack_panel) = parent.upgrade() { if !stack_panel.read(cx).is_last_panel(cx) { return false; } } } self.panels.len() <= 1 } /// Return all visible panels fn visible_panels<'a>(&'a self, cx: &'a App) -> impl Iterator> + 'a { self.panels.iter().filter_map(|panel| { if panel.visible(cx) { Some(panel.clone()) } else { None } }) } /// Return true if the tab panel is draggable. /// /// E.g. if the parent and self only have one panel, it is not draggable. fn draggable(&self, cx: &App) -> bool { !self.is_locked(cx) && !self.is_last_panel(cx) } /// Return true if the tab panel is droppable. /// /// E.g. if the tab panel is locked, it is not droppable. fn droppable(&self, cx: &App) -> bool { !self.is_locked(cx) } fn render_toolbar( &self, state: &TabState, window: &mut Window, cx: &mut Context, ) -> impl IntoElement { let is_zoomed = self.is_zoomed; let view = cx.entity().clone(); let zoomable_toolbar_visible = state.zoomable.map_or(false, |v| v.toolbar_visible()); // TODO: Do not show MenuButton if there is no menu items h_flex() .gap_2() .occlude() .items_center() .when_some(self.toolbar_buttons(window, cx), |this, buttons| { this.children(buttons.into_iter().map(|btn| btn.xsmall().ghost())) }) .map(|this| { let value = if is_zoomed { Some(("zoom-out", IconName::Minimize, t!("Dock.Zoom Out"))) } else if zoomable_toolbar_visible { Some(("zoom-in", IconName::Maximize, t!("Dock.Zoom In"))) } else { None }; if let Some((id, icon, tooltip)) = value { this.child( Button::new(id) .icon(icon) .xsmall() .ghost() .tooltip(tooltip) .on_click(cx.listener(|view, _, window, cx| { view.on_action_toggle_zoom(&ToggleZoom, window, cx) })), ) } else { this } }) .child( Button::new("menu") .icon(IconName::Ellipsis) .xsmall() .ghost() .popup_menu({ let zoomable = state.zoomable.map_or(false, |v| v.menu_visible()); let closable = state.closable; move |this, window, cx| { view.read(cx) .popup_menu(this, window, cx) .when(zoomable, |this| { let name = if is_zoomed { t!("Dock.Zoom Out") } else { t!("Dock.Zoom In") }; this.separator().menu(name, Box::new(ToggleZoom)) }) .when(closable, |this| { this.separator() .menu(t!("Dock.Close"), Box::new(ClosePanel)) }) } }) .anchor(Corner::TopRight), ) } fn render_dock_toggle_button( &self, placement: DockPlacement, _: &mut Window, cx: &mut Context, ) -> Option { if self.is_zoomed { return None; } let dock_area = self.dock_area.upgrade()?.read(cx); if !dock_area.is_dock_collapsible(placement, cx) { return None; } let view_entity_id = cx.entity().entity_id(); let toggle_button_panels = dock_area.toggle_button_panels; // Check if current TabPanel's entity_id matches the one stored in DockArea for this placement if !match placement { DockPlacement::Left => { dock_area.left_dock.is_some() && toggle_button_panels.left == Some(view_entity_id) } DockPlacement::Right => { dock_area.right_dock.is_some() && toggle_button_panels.right == Some(view_entity_id) } DockPlacement::Bottom => { dock_area.bottom_dock.is_some() && toggle_button_panels.bottom == Some(view_entity_id) } DockPlacement::Center => unreachable!(), } { return None; } let is_open = dock_area.is_dock_open(placement, cx); let icon = match placement { DockPlacement::Left => { if is_open { IconName::PanelLeft } else { IconName::PanelLeftOpen } } DockPlacement::Right => { if is_open { IconName::PanelRight } else { IconName::PanelRightOpen } } DockPlacement::Bottom => { if is_open { IconName::PanelBottom } else { IconName::PanelBottomOpen } } DockPlacement::Center => unreachable!(), }; Some( Button::new(SharedString::from(format!("toggle-dock:{:?}", placement))) .icon(icon) .xsmall() .ghost() .tooltip(match is_open { true => t!("Dock.Collapse"), false => t!("Dock.Expand"), }) .on_click(cx.listener({ let dock_area = self.dock_area.clone(); move |_, _, window, cx| { _ = dock_area.update(cx, |dock_area, cx| { dock_area.toggle_dock(placement, window, cx); }); } })), ) } fn render_title_bar( &self, state: &TabState, window: &mut Window, cx: &mut Context, ) -> impl IntoElement { let view = cx.entity().clone(); let Some(dock_area) = self.dock_area.upgrade() else { return div().into_any_element(); }; let panel_style = dock_area.read(cx).panel_style; let left_dock_button = self.render_dock_toggle_button(DockPlacement::Left, window, cx); let bottom_dock_button = self.render_dock_toggle_button(DockPlacement::Bottom, window, cx); let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx); if self.panels.len() == 1 && panel_style == PanelStyle::Default { let panel = self.panels.get(0).unwrap(); if !panel.visible(cx) { return div().into_any_element(); } let title_style = panel.title_style(cx); return h_flex() .justify_between() .items_center() .line_height(rems(1.0)) .h(px(30.)) .py_2() .px_3() .when(left_dock_button.is_some(), |this| this.pl_2()) .when(right_dock_button.is_some(), |this| this.pr_2()) .when_some(title_style, |this, theme| { this.bg(theme.background).text_color(theme.foreground) }) .when( left_dock_button.is_some() || bottom_dock_button.is_some(), |this| { this.child( h_flex() .flex_shrink_0() .mr_1() .gap_1() .children(left_dock_button) .children(bottom_dock_button), ) }, ) .child( div() .id("tab") .flex_1() .min_w_16() .overflow_hidden() .text_ellipsis() .whitespace_nowrap() .child(panel.title(window, cx)) .when(state.draggable, |this| { this.on_drag( DragPanel { panel: panel.clone(), tab_panel: view, }, |drag, _, _, cx| { cx.stop_propagation(); cx.new(|_| drag.clone()) }, ) }), ) .child( h_flex() .flex_shrink_0() .ml_1() .gap_1() .child(self.render_toolbar(&state, window, cx)) .children(right_dock_button), ) .into_any_element(); } let tabs_count = self.panels.len(); TabBar::new("tab-bar") .track_scroll(self.tab_bar_scroll_handle.clone()) .when( left_dock_button.is_some() || bottom_dock_button.is_some(), |this| { this.prefix( h_flex() .items_center() .top_0() // Right -1 for avoid border overlap with the first tab .right(-px(1.)) .border_r_1() .border_b_1() .h_full() .border_color(cx.theme().border) .bg(cx.theme().tab_bar) .px_2() .children(left_dock_button) .children(bottom_dock_button), ) }, ) .children(self.panels.iter().enumerate().filter_map(|(ix, panel)| { let mut active = state.active_panel.as_ref() == Some(panel); let disabled = self.is_collapsed; if !panel.visible(cx) { return None; } // Always not show active tab style, if the panel is collapsed if self.is_collapsed { active = false; } Some( Tab::new(("tab", ix), panel.title(window, cx)) .py_2() .selected(active) .disabled(disabled) .when(!disabled, |this| { this.on_click(cx.listener(move |view, _, window, cx| { view.set_active_ix(ix, window, cx); })) .when(state.draggable, |this| { this.on_drag( DragPanel::new(panel.clone(), view.clone()), |drag, _, _, cx| { cx.stop_propagation(); cx.new(|_| drag.clone()) }, ) }) .when(state.droppable, |this| { this.drag_over::(|this, _, _, cx| { this.rounded_l_none() .border_l_2() .border_r_0() .border_color(cx.theme().drag_border) }) .on_drop(cx.listener( move |this, drag: &DragPanel, window, cx| { this.will_split_placement = None; this.on_drop(drag, Some(ix), true, window, cx) }, )) }) }), ) })) .child( // empty space to allow move to last tab right div() .id("tab-bar-empty-space") .h_full() .flex_grow() .min_w_16() .when(state.droppable, |this| { this.drag_over::(|this, _, _, cx| { this.bg(cx.theme().drop_target) }) .on_drop(cx.listener( move |this, drag: &DragPanel, window, cx| { this.will_split_placement = None; let ix = if drag.tab_panel == view { Some(tabs_count - 1) } else { None }; this.on_drop(drag, ix, false, window, cx) }, )) }), ) .suffix( h_flex() .items_center() .top_0() .right_0() .border_l_1() .border_b_1() .h_full() .border_color(cx.theme().border) .bg(cx.theme().tab_bar) .px_2() .gap_1() .child(self.render_toolbar(state, window, cx)) .when_some(right_dock_button, |this, btn| this.child(btn)), ) .into_any_element() } fn render_active_panel( &self, state: &TabState, _: &mut Window, cx: &mut Context, ) -> impl IntoElement { if self.is_collapsed { return Empty {}.into_any_element(); } let Some(active_panel) = state.active_panel.as_ref() else { return Empty {}.into_any_element(); }; v_flex() .id("tab-content") .group("") .overflow_y_scroll() .overflow_x_hidden() .flex_1() .child( active_panel .view() .cached(StyleRefinement::default().v_flex().size_full()), ) .when(state.droppable, |this| { this.on_drag_move(cx.listener(Self::on_panel_drag_move)) .child( div() .invisible() .absolute() .bg(cx.theme().drop_target) .map(|this| match self.will_split_placement { Some(placement) => { let size = DefiniteLength::Fraction(0.35); match placement { Placement::Left => this.left_0().top_0().bottom_0().w(size), Placement::Right => { this.right_0().top_0().bottom_0().w(size) } Placement::Top => this.top_0().left_0().right_0().h(size), Placement::Bottom => { this.bottom_0().left_0().right_0().h(size) } } } None => this.top_0().left_0().size_full(), }) .group_drag_over::("", |this| this.visible()) .on_drop(cx.listener(|this, drag: &DragPanel, window, cx| { this.on_drop(drag, None, true, window, cx) })), ) }) .into_any_element() } /// Calculate the split direction based on the current mouse position fn on_panel_drag_move( &mut self, drag: &DragMoveEvent, _: &mut Window, cx: &mut Context, ) { let bounds = drag.bounds; let position = drag.event.position; // Check the mouse position to determine the split direction if position.x < bounds.left() + bounds.size.width * 0.35 { self.will_split_placement = Some(Placement::Left); } else if position.x > bounds.left() + bounds.size.width * 0.65 { self.will_split_placement = Some(Placement::Right); } else if position.y < bounds.top() + bounds.size.height * 0.35 { self.will_split_placement = Some(Placement::Top); } else if position.y > bounds.top() + bounds.size.height * 0.65 { self.will_split_placement = Some(Placement::Bottom); } else { // center to merge into the current tab self.will_split_placement = None; } cx.notify() } /// Handle the drop event when dragging a panel /// /// - `active` - When true, the panel will be active after the drop fn on_drop( &mut self, drag: &DragPanel, ix: Option, active: bool, window: &mut Window, cx: &mut Context, ) { let panel = drag.panel.clone(); let is_same_tab = drag.tab_panel == cx.entity(); // If target is same tab, and it is only one panel, do nothing. if is_same_tab && ix.is_none() { if self.will_split_placement.is_none() { return; } else { if self.panels.len() == 1 { return; } } } // Here is looks like remove_panel on a same item, but it difference. // // We must to split it to remove_panel, unless it will be crash by error: // Cannot update ui::dock::tab_panel::TabPanel while it is already being updated if is_same_tab { self.detach_panel(panel.clone(), window, cx); } else { let _ = drag.tab_panel.update(cx, |view, cx| { view.detach_panel(panel.clone(), window, cx); view.remove_self_if_empty(window, cx); }); } // Insert into new tabs if let Some(placement) = self.will_split_placement { self.split_panel(panel, placement, None, window, cx); } else { if let Some(ix) = ix { self.insert_panel_at(panel, ix, window, cx) } else { self.add_panel_with_active(panel, active, window, cx) } } self.remove_self_if_empty(window, cx); cx.emit(PanelEvent::LayoutChanged); } /// Add panel with split placement fn split_panel( &self, panel: Arc, placement: Placement, size: Option, window: &mut Window, cx: &mut Context, ) { let dock_area = self.dock_area.clone(); // wrap the panel in a TabPanel let new_tab_panel = cx.new(|cx| Self::new(None, dock_area.clone(), window, cx)); new_tab_panel.update(cx, |view, cx| { view.add_panel(panel, window, cx); }); let stack_panel = match self.stack_panel.as_ref().and_then(|panel| panel.upgrade()) { Some(panel) => panel, None => return, }; let parent_axis = stack_panel.read(cx).axis; let ix = stack_panel .read(cx) .index_of_panel(Arc::new(cx.entity().clone())) .unwrap_or_default(); if parent_axis.is_vertical() && placement.is_vertical() { stack_panel.update(cx, |view, cx| { view.insert_panel_at( Arc::new(new_tab_panel), ix, placement, size, dock_area.clone(), window, cx, ); }); } else if parent_axis.is_horizontal() && placement.is_horizontal() { stack_panel.update(cx, |view, cx| { view.insert_panel_at( Arc::new(new_tab_panel), ix, placement, size, dock_area.clone(), window, cx, ); }); } else { // 1. Create new StackPanel with new axis // 2. Move cx.entity() from parent StackPanel to the new StackPanel // 3. Add the new TabPanel to the new StackPanel at the correct index // 4. Add new StackPanel to the parent StackPanel at the correct index let tab_panel = cx.entity().clone(); // Try to use the old stack panel, not just create a new one, to avoid too many nested stack panels let new_stack_panel = if stack_panel.read(cx).panels_len() <= 1 { stack_panel.update(cx, |view, cx| { view.remove_all_panels(window, cx); view.set_axis(placement.axis(), window, cx); }); stack_panel.clone() } else { cx.new(|cx| { let mut panel = StackPanel::new(placement.axis(), window, cx); panel.parent = Some(stack_panel.downgrade()); panel }) }; new_stack_panel.update(cx, |view, cx| match placement { Placement::Left | Placement::Top => { view.add_panel(Arc::new(new_tab_panel), size, dock_area.clone(), window, cx); view.add_panel( Arc::new(tab_panel.clone()), None, dock_area.clone(), window, cx, ); } Placement::Right | Placement::Bottom => { view.add_panel( Arc::new(tab_panel.clone()), None, dock_area.clone(), window, cx, ); view.add_panel(Arc::new(new_tab_panel), size, dock_area.clone(), window, cx); } }); if stack_panel != new_stack_panel { stack_panel.update(cx, |view, cx| { view.replace_panel( Arc::new(tab_panel.clone()), new_stack_panel.clone(), window, cx, ); }); } cx.spawn_in(window, |_, mut cx| async move { cx.update(|window, cx| { tab_panel.update(cx, |view, cx| view.remove_self_if_empty(window, cx)) }) }) .detach() } cx.emit(PanelEvent::LayoutChanged); } fn focus_active_panel(&self, window: &mut Window, cx: &mut Context) { if let Some(active_panel) = self.active_panel(cx) { active_panel.focus_handle(cx).focus(window); } } fn on_action_toggle_zoom( &mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context, ) { if self.zoomable(cx).is_none() { return; } if !self.is_zoomed { cx.emit(PanelEvent::ZoomIn) } else { cx.emit(PanelEvent::ZoomOut) } self.is_zoomed = !self.is_zoomed; cx.spawn_in(window, |view, mut cx| { let is_zoomed = self.is_zoomed; async move { _ = cx.update(|window, cx| { _ = view.update(cx, |view, cx| { view.set_zoomed(is_zoomed, window, cx); }); }); } }) .detach(); } fn on_action_close_panel( &mut self, _: &ClosePanel, window: &mut Window, cx: &mut Context, ) { if let Some(panel) = self.active_panel(cx) { self.remove_panel(panel, window, cx); } } } impl Focusable for TabPanel { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { if let Some(active_panel) = self.active_panel(cx) { active_panel.focus_handle(cx) } else { self.focus_handle.clone() } } } impl EventEmitter for TabPanel {} impl EventEmitter for TabPanel {} impl Render for TabPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { let focus_handle = self.focus_handle(cx); let active_panel = self.active_panel(cx); let mut state = TabState { closable: self.closable(cx), draggable: self.draggable(cx), droppable: self.droppable(cx), zoomable: self.zoomable(cx), active_panel, }; if !state.draggable { state.closable = false; } v_flex() .id("tab-panel") .track_focus(&focus_handle) .on_action(cx.listener(Self::on_action_toggle_zoom)) .on_action(cx.listener(Self::on_action_close_panel)) .size_full() .overflow_hidden() .bg(cx.theme().background) .child(self.render_title_bar(&state, window, cx)) .child(self.render_active_panel(&state, window, cx)) } }