diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index 6de14bd8..ffcb318b 100644 --- a/crates/app/src/story_workspace.rs +++ b/crates/app/src/story_workspace.rs @@ -18,7 +18,7 @@ use ui::{ modal::Modal, popup_menu::PopupMenuExt, theme::{ActiveTheme, Colorize as _, Theme}, - ContextModal, IconName, Root, Sizable, + ContextModal, IconName, Placement, Root, Sizable, }; use crate::app_state::AppState; @@ -52,35 +52,49 @@ impl StoryWorkspace { .detach(); let stack_panel = cx.new_view(|cx| StackPanel::new(Axis::Horizontal, cx)); - let dock_area = cx.new_view(|cx| DockArea::new(stack_panel.clone(), cx)); + let dock_area = cx.new_view(|cx| DockArea::new("main-dock", stack_panel.clone(), cx)); let weak_dock_area = dock_area.downgrade(); - let tab_panel = cx.new_view(|cx| TabPanel::new(weak_dock_area.clone(), cx)); - let right_tab_panel = cx.new_view(|cx| TabPanel::new(weak_dock_area.clone(), cx)); - let right_tab_panel1 = cx.new_view(|cx| TabPanel::new(weak_dock_area.clone(), cx)); + let center_tab_panel = + cx.new_view(|cx| TabPanel::new(Some(stack_panel.clone()), weak_dock_area.clone(), cx)); + let left_tab_panel = + cx.new_view(|cx| TabPanel::new(Some(stack_panel.clone()), weak_dock_area.clone(), cx)); + let right_tab_panel = + cx.new_view(|cx| TabPanel::new(Some(stack_panel.clone()), weak_dock_area.clone(), cx)); stack_panel.update(cx, |view, cx| { - view.add_panel(tab_panel.clone(), None, weak_dock_area.clone(), cx); - - let stock_panel1 = cx.new_view(|cx| StackPanel::new(Axis::Vertical, cx)); + let left_stack_panel = cx.new_view(|cx| StackPanel::new(Axis::Vertical, cx)); + left_stack_panel.update(cx, |view, cx| { + view.add_panel(left_tab_panel.clone(), None, weak_dock_area.clone(), cx); + }); view.add_panel( - stock_panel1.clone(), - Some(px(380.)), + left_stack_panel.clone(), + Some(px(300.)), weak_dock_area.clone(), cx, ); - stock_panel1.update(cx, |view, cx| { + view.add_panel(center_tab_panel.clone(), None, weak_dock_area.clone(), cx); + + let right_stack_panel = cx.new_view(|cx| StackPanel::new(Axis::Vertical, cx)); + right_stack_panel.update(cx, |view, cx| { view.add_panel(right_tab_panel.clone(), None, weak_dock_area.clone(), cx); - view.add_panel(right_tab_panel1.clone(), None, weak_dock_area.clone(), cx); - }) + }); + view.add_panel( + right_stack_panel.clone(), + Some(px(340.)), + weak_dock_area.clone(), + cx, + ); }); StoryContainer::add_panel( "Buttons", "Displays a button or a component that looks like a button.", ButtonStory::view(cx).into(), - tab_panel.clone(), + center_tab_panel.clone(), + None, + None, false, cx, ) @@ -90,7 +104,9 @@ impl StoryWorkspace { "Input", "A control that allows the user to input text.", InputStory::view(cx).into(), - tab_panel.clone(), + center_tab_panel.clone(), + None, + None, false, cx, ) @@ -100,7 +116,9 @@ impl StoryWorkspace { "Text", "Links, paragraphs, checkboxes, and more.", TextStory::view(cx).into(), - tab_panel.clone(), + center_tab_panel.clone(), + Some(Placement::Bottom), + Some(px(200.)), true, cx, ) @@ -110,7 +128,9 @@ impl StoryWorkspace { "Switch", "A control that allows the user to toggle between two states.", SwitchStory::view(cx).into(), - tab_panel.clone(), + center_tab_panel.clone(), + None, + None, true, cx, ) @@ -120,7 +140,9 @@ impl StoryWorkspace { "Dropdowns", "Displays a list of options for the user to pick from—triggered by a button.", DropdownStory::new(cx).into(), - tab_panel.clone(), + center_tab_panel.clone(), + None, + None, true, cx, ) @@ -130,7 +152,9 @@ impl StoryWorkspace { "Modal", "Modal & Drawer use examples", ModalStory::view(cx).into(), - tab_panel.clone(), + center_tab_panel.clone(), + None, + None, true, cx, ) @@ -140,7 +164,9 @@ impl StoryWorkspace { "Popup", "A popup displays content on top of the main page.", PopupStory::view(cx).into(), - tab_panel.clone(), + center_tab_panel.clone(), + None, + None, true, cx, ) @@ -150,7 +176,9 @@ impl StoryWorkspace { "Tooltip", "Displays a short message when users hover over an element.", TooltipStory::view(cx).into(), - tab_panel.clone(), + right_tab_panel.clone(), + Some(Placement::Top), + None, true, cx, ) @@ -160,7 +188,9 @@ impl StoryWorkspace { "List", "A list displays a series of items.", ListStory::view(cx).into(), - tab_panel.clone(), + left_tab_panel.clone(), + None, + None, true, cx, ) @@ -170,7 +200,9 @@ impl StoryWorkspace { "Icon", "Icon use examples", IconStory::view(cx).into(), - tab_panel.clone(), + left_tab_panel.clone(), + Some(Placement::Bottom), + Some(px(200.)), true, cx, ) @@ -180,7 +212,9 @@ impl StoryWorkspace { "Image", "Render SVG image and Chart", ImageStory::view(cx).into(), - right_tab_panel1.clone(), + right_tab_panel.clone(), + None, + None, true, cx, ) @@ -198,7 +232,9 @@ impl StoryWorkspace { "Table", "Powerful table and datagrids built.", TableStory::view(cx).into(), - tab_panel.clone(), + center_tab_panel.clone(), + None, + None, true, cx, ) @@ -208,8 +244,10 @@ impl StoryWorkspace { "Progress", "Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.", ProgressStory::view(cx).into(), - tab_panel.clone(), - true, + center_tab_panel.clone(), + Some(Placement::Bottom), + Some(px(200.)), + true, cx, ) .detach(); @@ -218,7 +256,9 @@ impl StoryWorkspace { "Resizable", "Accessible resizable panel groups and layouts with keyboard support.", ResizableStory::view(cx).into(), - tab_panel.clone(), + center_tab_panel.clone(), + None, + None, true, cx, ) @@ -228,7 +268,9 @@ impl StoryWorkspace { "Scrollable", "A scrollable area with scroll bar.", ScrollableStory::view(cx).into(), - tab_panel.clone(), + center_tab_panel.clone(), + None, + None, true, cx, ) @@ -238,7 +280,9 @@ impl StoryWorkspace { "Calendar", "A calendar component.", CalendarStory::view(cx).into(), - right_tab_panel.clone(), + center_tab_panel.clone(), + None, + None, true, cx, ) diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index 6edb4231..d1e7e20d 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -38,7 +38,7 @@ pub use webview_story::WebViewStory; use gpui::{ actions, div, prelude::FluentBuilder as _, px, AnyView, AppContext, Div, EventEmitter, - FocusableView, InteractiveElement, IntoElement, ParentElement, Render, SharedString, + FocusableView, InteractiveElement, IntoElement, ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled as _, Task, View, ViewContext, VisualContext, WindowContext, }; @@ -49,7 +49,7 @@ use ui::{ h_flex, label::Label, popup_menu::PopupMenu, - v_flex, + v_flex, Placement, }; pub fn init(cx: &mut AppContext) { @@ -120,11 +120,14 @@ impl StoryContainer { } } + #[allow(clippy::too_many_arguments)] pub fn add_panel( name: impl Into, description: impl Into, story: AnyView, tab_panel: View, + placement: Option, + size: Option, closeable: bool, cx: &mut WindowContext, ) -> Task>> { @@ -135,7 +138,11 @@ impl StoryContainer { tab_panel.update(&mut cx, |panel, cx| { let view = cx.new_view(|cx| Self::new(name, description, closeable, cx).story(story)); - panel.add_panel(Arc::new(view.clone()), cx); + if let Some(placement) = placement { + panel.add_panel_at(Arc::new(view.clone()), placement, size, cx); + } else { + panel.add_panel(Arc::new(view.clone()), cx); + } view }) }) diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index fd8785ad..72afb1ff 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -4,7 +4,7 @@ mod tab_panel; use gpui::{ actions, div, prelude::FluentBuilder, AnyWeakView, InteractiveElement as _, IntoElement, - ParentElement as _, Render, Styled, View, ViewContext, + ParentElement as _, Render, SharedString, Styled, View, ViewContext, }; pub use panel::*; pub use stack_panel::*; @@ -14,18 +14,29 @@ actions!(dock, [ToggleZoom, ClosePanel]); /// The main area of the dock. pub struct DockArea { + id: SharedString, root: View, zoom_view: Option, } impl DockArea { - pub fn new(root: View, _cx: &mut ViewContext) -> Self { + pub fn new( + id: impl Into, + root: View, + _cx: &mut ViewContext, + ) -> Self { Self { + id: id.into(), root, zoom_view: None, } } + /// Returns the ID of the dock area. + pub fn id(&self) -> SharedString { + self.id.clone() + } + /// Toggles the zoom view. pub fn toggle_zoom(&mut self, panel: View

, cx: &mut ViewContext) { if self.zoom_view.is_some() { @@ -35,6 +46,25 @@ impl DockArea { } cx.notify(); } + + /// Returns the root stack panel. + pub fn root(&self) -> View { + self.root.clone() + } + + /// Return the index of the panel. + pub fn index_of_panel( + &self, + panel: View

, + cx: &mut ViewContext, + ) -> Option { + self.root.read(cx).index_of_panel(panel) + } + + /// Return the existing panel by type. + pub fn panel(&self, cx: &mut ViewContext) -> Option> { + self.root.read(cx).panel::

(cx) + } } impl Render for DockArea { diff --git a/crates/ui/src/dock/stack_panel.rs b/crates/ui/src/dock/stack_panel.rs index e44ef468..ed4f4c2c 100644 --- a/crates/ui/src/dock/stack_panel.rs +++ b/crates/ui/src/dock/stack_panel.rs @@ -11,7 +11,7 @@ use super::{DockArea, Panel, PanelEvent, PanelView, TabPanel}; use gpui::{ prelude::FluentBuilder as _, Axis, DismissEvent, Entity, EventEmitter, FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, View, ViewContext, - VisualContext, WeakView, + VisualContext, WeakView, WindowContext, }; use smallvec::SmallVec; @@ -62,6 +62,24 @@ impl StackPanel { .position(|p| p.view().entity_id() == entity_id) } + /// Return the existing panel view by type. + pub fn panel

(&self, cx: &WindowContext) -> Option> + where + P: Panel, + { + self.panels.iter().find_map(|p| { + if let Ok(p) = p.view().downcast::

() { + Some(p) + } else { + if let Ok(tab) = p.view().downcast::() { + tab.read(cx).panel(cx) + } else { + None + } + } + }) + } + /// Add a panel at the end of the stack. pub fn add_panel

( &mut self, @@ -76,19 +94,35 @@ impl StackPanel { } pub fn add_panel_at

( + &mut self, + panel: View

, + placement: Placement, + size: Option, + dock_area: WeakView, + cx: &mut ViewContext, + ) where + P: Panel, + { + self.insert_panel_at(panel, self.panels_len(), placement, size, dock_area, cx); + } + + pub fn insert_panel_at

( &mut self, panel: View

, ix: usize, placement: Placement, + size: Option, dock_area: WeakView, cx: &mut ViewContext, ) where P: Panel, { match placement { - Placement::Top | Placement::Left => self.insert_panel_before(panel, ix, dock_area, cx), + Placement::Top | Placement::Left => { + self.insert_panel_before(panel, ix, size, dock_area, cx) + } Placement::Right | Placement::Bottom => { - self.insert_panel_after(panel, ix, dock_area, cx) + self.insert_panel_after(panel, ix, size, dock_area, cx) } } } @@ -98,12 +132,13 @@ impl StackPanel { &mut self, panel: View

, ix: usize, + size: Option, dock_area: WeakView, cx: &mut ViewContext, ) where P: Panel, { - self.insert_panel(panel, ix, None, dock_area, cx); + self.insert_panel(panel, ix, size, dock_area, cx); } /// Insert a panel after the index. @@ -111,12 +146,13 @@ impl StackPanel { &mut self, panel: View

, ix: usize, + size: Option, dock_area: WeakView, cx: &mut ViewContext, ) where P: Panel, { - self.insert_panel(panel, ix + 1, None, dock_area, cx); + self.insert_panel(panel, ix + 1, size, dock_area, cx); } fn new_resizable_panel

(panel: View

, size: Option) -> ResizablePanel @@ -176,6 +212,12 @@ impl StackPanel { }) .detach(); + let ix = if ix > self.panels.len() { + self.panels.len() + } else { + ix + }; + self.panels.insert(ix, Arc::new(panel.clone())); self.panel_group.update(cx, |view, cx| { view.insert_child(Self::new_resizable_panel(panel, size), ix, cx) diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock/tab_panel.rs index f80ba292..b5cc7348 100644 --- a/crates/ui/src/dock/tab_panel.rs +++ b/crates/ui/src/dock/tab_panel.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use gpui::{ div, prelude::FluentBuilder, rems, AnchorCorner, AppContext, DefiniteLength, DismissEvent, DragMoveEvent, Empty, EventEmitter, FocusHandle, FocusableView, InteractiveElement as _, - IntoElement, ParentElement, Render, ScrollHandle, StatefulInteractiveElement, Styled, View, - ViewContext, VisualContext as _, WeakView, WindowContext, + IntoElement, ParentElement, Pixels, Render, ScrollHandle, StatefulInteractiveElement, Styled, + View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use rust_i18n::t; @@ -71,11 +71,15 @@ pub struct TabPanel { } impl TabPanel { - pub fn new(dock_area: WeakView, cx: &mut ViewContext) -> Self { + pub fn new( + stack_panel: Option>, + dock_area: WeakView, + cx: &mut ViewContext, + ) -> Self { Self { focus_handle: cx.focus_handle(), dock_area, - stack_panel: None, + stack_panel, panels: Vec::new(), active_ix: 0, tab_bar_scroll_handle: ScrollHandle::new(), @@ -99,6 +103,21 @@ impl TabPanel { cx.notify(); } + /// Return the existing panel view by type. + pub fn panel(&self, cx: &WindowContext) -> Option> { + self.panels.iter().find_map(|p| { + if let Ok(p) = p.view().downcast::

() { + Some(p) + } else { + if let Ok(stack) = p.view().downcast::() { + stack.read(cx).panel::

(cx) + } else { + None + } + } + }) + } + /// Add a panel to the end of the tabs pub fn add_panel(&mut self, panel: Arc, cx: &mut ViewContext) { if self @@ -115,6 +134,19 @@ impl TabPanel { cx.notify(); } + /// Add panel to try to split + pub fn add_panel_at( + &mut self, + panel: Arc, + placement: Placement, + size: Option, + cx: &mut ViewContext, + ) { + self.will_split_placement = Some(placement); + self.split_panel(panel, placement, size, cx); + cx.notify(); + } + fn insert_panel_at( &mut self, panel: Arc, @@ -401,7 +433,7 @@ impl TabPanel { // Insert into new tabs if let Some(placement) = self.will_split_placement { - self.split_panel(panel, placement, cx); + self.split_panel(panel, placement, None, cx); } else { if let Some(ix) = ix { self.insert_panel_at(panel, ix, cx) @@ -418,11 +450,12 @@ impl TabPanel { &self, panel: Arc, placement: Placement, + size: Option, cx: &mut ViewContext, ) { let dock_area = self.dock_area.clone(); // wrap the panel in a TabPanel - let new_tab_panel = cx.new_view(|cx| Self::new(dock_area.clone(), cx)); + let new_tab_panel = cx.new_view(|cx| Self::new(None, dock_area.clone(), cx)); new_tab_panel.update(cx, |view, cx| { view.add_panel(panel, cx); }); @@ -436,11 +469,11 @@ impl TabPanel { if parent_axis.is_vertical() && placement.is_vertical() { stack_panel.update(cx, |view, cx| { - view.add_panel_at(new_tab_panel, ix, placement, dock_area.clone(), cx); + view.insert_panel_at(new_tab_panel, ix, placement, size, dock_area.clone(), cx); }); } else if parent_axis.is_horizontal() && placement.is_horizontal() { stack_panel.update(cx, |view, cx| { - view.add_panel_at(new_tab_panel, ix, placement, dock_area.clone(), cx); + view.insert_panel_at(new_tab_panel, ix, placement, size, dock_area.clone(), cx); }); } else { // 1. Create new StackPanel with new axis @@ -466,12 +499,12 @@ impl TabPanel { new_stack_panel.update(cx, |view, cx| match placement { Placement::Left | Placement::Top => { - view.add_panel(new_tab_panel, None, dock_area.clone(), cx); + view.add_panel(new_tab_panel, size, dock_area.clone(), cx); view.add_panel(tab_panel.clone(), None, dock_area.clone(), cx); } Placement::Right | Placement::Bottom => { view.add_panel(tab_panel.clone(), None, dock_area.clone(), cx); - view.add_panel(new_tab_panel, None, dock_area.clone(), cx); + view.add_panel(new_tab_panel, size, dock_area.clone(), cx); } }); diff --git a/crates/ui/src/resizable/panel.rs b/crates/ui/src/resizable/panel.rs index 9887514d..5bb49844 100644 --- a/crates/ui/src/resizable/panel.rs +++ b/crates/ui/src/resizable/panel.rs @@ -2,8 +2,8 @@ use std::rc::Rc; use gpui::{ canvas, div, prelude::FluentBuilder, px, Along, AnyElement, AnyView, Axis, Bounds, Element, - EntityId, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement, - Pixels, Render, StatefulInteractiveElement, Style, Styled, View, ViewContext, + Entity, EntityId, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, + ParentElement, Pixels, Render, StatefulInteractiveElement, Style, Styled, View, ViewContext, VisualContext as _, WindowContext, }; @@ -89,26 +89,19 @@ impl ResizablePanelGroup { self } - /// When add a child panel to the group, resize other panels each into half of the remaining space. - fn default_panel_size(&self) -> Pixels { - let container_size = self.bounds.size.along(self.axis); - let each_size = container_size / (self.panels.len() + 1) as f32; - each_size.round() - } - pub fn add_child(&mut self, panel: ResizablePanel, cx: &mut ViewContext) { let mut panel = panel; panel.axis = self.axis; - panel.size = self.default_panel_size(); - self.sizes.push(panel.size); + panel.group = Some(cx.view().clone()); + self.sizes.push(panel.size.unwrap_or_default()); self.panels.push(cx.new_view(|_| panel)); } pub fn insert_child(&mut self, panel: ResizablePanel, ix: usize, cx: &mut ViewContext) { let mut panel = panel; panel.axis = self.axis; - panel.size = self.default_panel_size(); - self.sizes.insert(ix, panel.size); + panel.group = Some(cx.view().clone()); + self.sizes.insert(ix, panel.size.unwrap_or_default()); self.panels.insert(ix, cx.new_view(|_| panel)); cx.notify() } @@ -122,8 +115,8 @@ impl ResizablePanelGroup { ) { let mut panel = panel; panel.axis = self.axis; - panel.size = self.default_panel_size(); - self.sizes[ix] = panel.size; + panel.group = Some(cx.view().clone()); + self.sizes[ix] = panel.size.unwrap_or_default(); self.panels[ix] = cx.new_view(|_| panel); cx.notify() } @@ -142,7 +135,7 @@ impl ResizablePanelGroup { fn render_resize_handle(&self, ix: usize, cx: &mut ViewContext) -> impl IntoElement { let axis = self.axis; - let neg_offset = -HANDLE_PADDING + px(1.); + let neg_offset = -HANDLE_PADDING; let view = cx.view().clone(); div() @@ -153,14 +146,14 @@ impl ResizablePanelGroup { .when(self.axis.is_horizontal(), |this| { this.cursor_col_resize() .top_0() - .right(neg_offset) + .left(neg_offset) .h_full() .w(px(1.)) .px(HANDLE_PADDING) }) .when(self.axis.is_vertical(), |this| { this.cursor_row_resize() - .bottom(neg_offset) + .top(neg_offset) .left_0() .w_full() .h(px(1.)) @@ -250,7 +243,9 @@ impl ResizablePanelGroup { self.sizes = new_sizes; for (i, panel) in self.panels.iter().enumerate() { let size = self.sizes[i]; - panel.update(cx, |this, _| this.size = size); + if size > px(0.) { + panel.update(cx, |this, _| this.changed_size = Some(size)); + } } } } @@ -267,8 +262,8 @@ impl Render for ResizablePanelGroup { container .size_full() .children(self.panels.iter().enumerate().map(|(ix, panel)| { - if ix < self.panels.len() - 1 { - let handle = self.render_resize_handle(ix, cx); + if ix > 0 { + let handle = self.render_resize_handle(ix - 1, cx); panel.update(cx, |view, _| { view.resize_handle = Some(handle.into_any_element()) }); @@ -292,7 +287,9 @@ impl Render for ResizablePanelGroup { } pub struct ResizablePanel { - size: Pixels, + group: Option>, + size: Option, + changed_size: Option, axis: Axis, content_builder: Option AnyElement>>, content_view: Option, @@ -304,7 +301,9 @@ pub struct ResizablePanel { impl ResizablePanel { pub(super) fn new() -> Self { Self { - size: PANEL_MIN_SIZE, + group: None, + size: None, + changed_size: None, axis: Axis::Horizontal, content_builder: None, content_view: None, @@ -327,9 +326,29 @@ impl ResizablePanel { } pub fn size(mut self, size: Pixels) -> Self { - self.size = size; + self.size = Some(size); self } + + /// Save the real panel size, and update group sizes + fn update_size(&mut self, bounds: Bounds, cx: &mut ViewContext) { + let new_size = bounds.size.along(self.axis); + self.bounds = bounds; + + let panel_view = cx.view().clone(); + if let Some(group) = self.group.as_ref() { + group.update(cx, |view, _| { + if let Some(ix) = view + .panels + .iter() + .position(|v| v.entity_id() == panel_view.entity_id()) + { + view.sizes[ix] = new_size; + } + }) + } + cx.notify(); + } } impl FluentBuilder for ResizablePanel {} @@ -337,24 +356,27 @@ impl FluentBuilder for ResizablePanel {} impl Render for ResizablePanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let view = cx.view().clone(); - let axis = self.axis; - let size = self.size.max(PANEL_MIN_SIZE); div() .flex() .flex_grow() + .size_full() .relative() - .overflow_hidden() - .when(self.axis.is_vertical(), |this| this.w_full().h(size)) - .when(self.axis.is_horizontal(), |this| this.h_full().w(size)) + .when(self.size.is_none(), |this| this.flex_shrink()) + .when(self.axis.is_vertical(), |this| this.min_h(PANEL_MIN_SIZE)) + .when(self.axis.is_horizontal(), |this| this.min_w(PANEL_MIN_SIZE)) + .when_some(self.size, |this, size| { + this.flex_shrink_0() + .when(self.axis.is_vertical(), |this| this.h(size)) + .when(self.axis.is_horizontal(), |this| this.w(size)) + }) + .when_some(self.changed_size, |this, size| { + this.when(self.axis.is_vertical(), |this| this.h(size)) + .when(self.axis.is_horizontal(), |this| this.w(size)) + }) .child({ canvas( - move |bounds, cx| { - view.update(cx, |r, _| { - r.size = bounds.size.along(axis); - r.bounds = bounds; - }) - }, + move |bounds, cx| view.update(cx, |r, cx| r.update_size(bounds, cx)), |_, _, _| {}, ) .absolute()