git_ui: Panel Horizontal Scroll (#26402)

Known Issues:
- When items can horizontal scroll, the right selected border is hidden

TODO:
- [ ] Width calculation is off
- [ ] When scrollbars should autohide they don't until hovering the
panel
- [ ] When switching to and from scrollbar track being visible we are
missing a notify somewhere.

Release Notes:

- Git Panel: Added horizontal scrolling in the git panel

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Nate Butler 2025-03-11 18:47:39 -04:00 committed by GitHub
parent 5d66c3db85
commit 09a4cfd307
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -28,9 +28,9 @@ use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
use gpui::{
actions, anchored, deferred, percentage, uniform_list, Action, Animation, AnimationExt as _,
ClickEvent, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Stateful, Subscription, Task,
Axis, ClickEvent, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task,
Transformation, UniformListScrollHandle, WeakEntity,
};
use itertools::Itertools;
@ -205,6 +205,21 @@ pub struct GitStatusEntry {
pub(crate) staging: StageStatus,
}
impl GitStatusEntry {
fn display_name(&self) -> String {
self.worktree_path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| self.worktree_path.to_string_lossy().into_owned())
}
fn parent_dir(&self) -> Option<String> {
self.worktree_path
.parent()
.map(|parent| parent.to_string_lossy().into_owned())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TargetStatus {
Staged,
@ -222,6 +237,61 @@ struct PendingOperation {
type RemoteOperations = Rc<RefCell<HashSet<u32>>>;
// computed state related to how to render scrollbars
// one per axis
// on render we just read this off the panel
// we update it when
// - settings change
// - on focus in, on focus out, on hover, etc.
#[derive(Debug)]
struct ScrollbarProperties {
axis: Axis,
show_scrollbar: bool,
show_track: bool,
auto_hide: bool,
hide_task: Option<Task<()>>,
state: ScrollbarState,
}
impl ScrollbarProperties {
// Shows the scrollbar and cancels any pending hide task
fn show(&mut self, cx: &mut Context<GitPanel>) {
if !self.auto_hide {
return;
}
self.show_scrollbar = true;
self.hide_task.take();
cx.notify();
}
fn hide(&mut self, window: &mut Window, cx: &mut Context<GitPanel>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
if !self.auto_hide {
return;
}
let axis = self.axis;
self.hide_task = Some(cx.spawn_in(window, |panel, mut cx| async move {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
if let Some(panel) = panel.upgrade() {
panel
.update(&mut cx, |panel, cx| {
match axis {
Axis::Vertical => panel.vertical_scrollbar.show_scrollbar = false,
Axis::Horizontal => panel.horizontal_scrollbar.show_scrollbar = false,
}
cx.notify();
})
.log_err();
}
}));
}
}
pub struct GitPanel {
remote_operation_id: u32,
pending_remote_operations: RemoteOperations,
@ -237,7 +307,8 @@ pub struct GitPanel {
single_tracked_entry: Option<GitStatusEntry>,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
hide_scrollbar_task: Option<Task<()>>,
horizontal_scrollbar: ScrollbarProperties,
vertical_scrollbar: ScrollbarProperties,
new_count: usize,
entry_count: usize,
new_staged_count: usize,
@ -246,10 +317,9 @@ pub struct GitPanel {
pending_serialization: Task<Option<()>>,
pub(crate) project: Entity<Project>,
scroll_handle: UniformListScrollHandle,
scrollbar_state: ScrollbarState,
max_width_item_index: Option<usize>,
selected_entry: Option<usize>,
marked_entries: Vec<usize>,
show_scrollbar: bool,
tracked_count: usize,
tracked_staged_count: usize,
update_visible_entries_task: Task<()>,
@ -316,7 +386,7 @@ impl GitPanel {
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
this.hide_scrollbar(window, cx);
this.hide_scrollbars(window, cx);
})
.detach();
@ -355,8 +425,23 @@ impl GitPanel {
)
.detach();
let scrollbar_state =
ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity());
let vertical_scrollbar = ScrollbarProperties {
axis: Axis::Vertical,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let horizontal_scrollbar = ScrollbarProperties {
axis: Axis::Horizontal,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let mut git_panel = Self {
pending_remote_operations: Default::default(),
@ -371,7 +456,6 @@ impl GitPanel {
entries: Vec::new(),
focus_handle: cx.focus_handle(),
fs,
hide_scrollbar_task: None,
new_count: 0,
new_staged_count: 0,
pending: Vec::new(),
@ -381,10 +465,9 @@ impl GitPanel {
single_tracked_entry: None,
project,
scroll_handle,
scrollbar_state,
max_width_item_index: None,
selected_entry: None,
marked_entries: Vec::new(),
show_scrollbar: false,
tracked_count: 0,
tracked_staged_count: 0,
update_visible_entries_task: Task::ready(()),
@ -393,12 +476,93 @@ impl GitPanel {
workspace,
modal_open: false,
entry_count: 0,
horizontal_scrollbar,
vertical_scrollbar,
};
git_panel.schedule_update(false, window, cx);
git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
git_panel
}
fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.horizontal_scrollbar.hide(window, cx);
self.vertical_scrollbar.hide(window, cx);
}
fn update_scrollbar_properties(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
// TODO: This PR should have defined Editor's `scrollbar.axis`
// as an Option<ScrollbarAxis>, not a ScrollbarAxes as it would allow you to
// `.unwrap_or(EditorSettings::get_global(cx).scrollbar.show)`.
//
// Once this is fixed we can extend the GitPanelSettings with a `scrollbar.axis`
// so we can show each axis based on the settings.
//
// We should fix this. PR: https://github.com/zed-industries/zed/pull/19495
let show_setting = GitPanelSettings::get_global(cx)
.scrollbar
.show
.unwrap_or(EditorSettings::get_global(cx).scrollbar.show);
let scroll_handle = self.scroll_handle.0.borrow();
let autohide = |show: ShowScrollbar, cx: &mut Context<Self>| match show {
ShowScrollbar::Auto => true,
ShowScrollbar::System => cx
.try_global::<ScrollbarAutoHide>()
.map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
ShowScrollbar::Always => false,
ShowScrollbar::Never => false,
};
let longest_item_width = scroll_handle.last_item_size.and_then(|size| {
(size.contents.width > size.item.width).then_some(size.contents.width)
});
// is there an item long enough that we should show a horizontal scrollbar?
let item_wider_than_container = if let Some(longest_item_width) = longest_item_width {
longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0)
} else {
true
};
let show_horizontal = match (show_setting, item_wider_than_container) {
(_, false) => false,
(ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always, true) => true,
(ShowScrollbar::Never, true) => false,
};
let show_vertical = match show_setting {
ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
ShowScrollbar::Never => false,
};
let show_horizontal_track =
show_horizontal && matches!(show_setting, ShowScrollbar::Always);
// TODO: we probably should hide the scroll track when the list doesn't need to scroll
let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always);
self.vertical_scrollbar = ScrollbarProperties {
axis: self.vertical_scrollbar.axis,
state: self.vertical_scrollbar.state.clone(),
show_scrollbar: show_vertical,
show_track: show_vertical_track,
auto_hide: autohide(show_setting, cx),
hide_task: None,
};
self.horizontal_scrollbar = ScrollbarProperties {
axis: self.horizontal_scrollbar.axis,
state: self.horizontal_scrollbar.state.clone(),
show_scrollbar: show_horizontal,
show_track: show_horizontal_track,
auto_hide: autohide(show_setting, cx),
hide_task: None,
};
cx.notify();
}
pub fn entry_by_path(&self, path: &RepoPath) -> Option<usize> {
fn binary_search<F>(mut low: usize, mut high: usize, is_target: F) -> Option<usize>
where
@ -555,53 +719,6 @@ impl GitPanel {
}
}
fn show_scrollbar(&self, cx: &mut Context<Self>) -> ShowScrollbar {
GitPanelSettings::get_global(cx)
.scrollbar
.show
.unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show)
}
fn should_show_scrollbar(&self, cx: &mut Context<Self>) -> bool {
let show = self.show_scrollbar(cx);
match show {
ShowScrollbar::Auto => true,
ShowScrollbar::System => true,
ShowScrollbar::Always => true,
ShowScrollbar::Never => false,
}
}
fn should_autohide_scrollbar(&self, cx: &mut Context<Self>) -> bool {
let show = self.show_scrollbar(cx);
match show {
ShowScrollbar::Auto => true,
ShowScrollbar::System => cx
.try_global::<ScrollbarAutoHide>()
.map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
ShowScrollbar::Always => false,
ShowScrollbar::Never => true,
}
}
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
if !self.should_autohide_scrollbar(cx) {
return;
}
self.hide_scrollbar_task = Some(cx.spawn_in(window, |panel, mut cx| async move {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
panel
.update(&mut cx, |panel, cx| {
panel.show_scrollbar = false;
cx.notify();
})
.log_err();
}))
}
fn handle_modifiers_changed(
&mut self,
event: &ModifiersChangedEvent,
@ -1972,12 +2089,13 @@ impl GitPanel {
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
if let Some(git_panel) = handle.upgrade() {
git_panel
.update_in(&mut cx, |git_panel, _, cx| {
.update_in(&mut cx, |git_panel, window, cx| {
if clear_pending {
git_panel.clear_pending();
}
git_panel.update_visible_entries(cx);
git_panel.update_editor_placeholder(cx);
git_panel.update_scrollbar_properties(window, cx);
})
.ok();
}
@ -2038,6 +2156,7 @@ impl GitPanel {
let mut conflict_entries = Vec::new();
let mut last_staged = None;
let mut staged_count = 0;
let mut max_width_item: Option<(RepoPath, usize)> = None;
let Some(repo) = self.active_repository.as_ref() else {
// Just clear entries if no repository is active.
@ -2083,6 +2202,21 @@ impl GitPanel {
last_staged = Some(entry.clone());
}
let width_estimate = Self::item_width_estimate(
entry.parent_dir().map(|s| s.len()).unwrap_or(0),
entry.display_name().len(),
);
match max_width_item.as_mut() {
Some((repo_path, estimate)) => {
if width_estimate > *estimate {
*repo_path = entry.repo_path.clone();
*estimate = width_estimate;
}
}
None => max_width_item = Some((entry.repo_path.clone(), width_estimate)),
}
if is_conflict {
conflict_entries.push(entry);
} else if is_new {
@ -2155,6 +2289,15 @@ impl GitPanel {
.extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry));
}
if let Some((repo_path, _)) = max_width_item {
self.max_width_item_index = self.entries.iter().position(|entry| match entry {
GitListEntry::GitStatusEntry(git_status_entry) => {
git_status_entry.repo_path == repo_path
}
GitListEntry::Header(_) => false,
});
}
self.update_counts(repo);
self.select_first_entry_if_none(cx);
@ -2376,6 +2519,12 @@ impl GitPanel {
self.has_staged_changes()
}
// eventually we'll need to take depth into account here
// if we add a tree view
fn item_width_estimate(path: usize, file_name: usize) -> usize {
path + file_name
}
fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();
PopoverMenu::new(id.into())
@ -2795,58 +2944,108 @@ impl GitPanel {
)
}
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
let scroll_bar_style = self.show_scrollbar(cx);
let show_container = matches!(scroll_bar_style, ShowScrollbar::Always);
fn render_vertical_scrollbar(
&self,
show_horizontal_scrollbar_container: bool,
cx: &mut Context<Self>,
) -> impl IntoElement {
div()
.id("git-panel-vertical-scroll")
.occlude()
.flex_none()
.h_full()
.cursor_default()
.absolute()
.right_0()
.top_0()
.bottom_0()
.w(px(12.))
.when(show_horizontal_scrollbar_container, |this| {
this.pb_neg_3p5()
})
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
if !this.vertical_scrollbar.state.is_dragging()
&& !this.focus_handle.contains_focused(window, cx)
{
this.vertical_scrollbar.hide(window, cx);
cx.notify();
}
if !self.should_show_scrollbar(cx)
|| !(self.show_scrollbar || self.scrollbar_state.is_dragging())
{
return None;
}
Some(
div()
.id("git-panel-vertical-scroll")
.occlude()
.flex_none()
.h_full()
.cursor_default()
.when(show_container, |this| this.pl_1().px_1p5())
.when(!show_container, |this| {
this.absolute().right_1().top_1().bottom_1().w(px(12.))
})
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
if !this.scrollbar_state.is_dragging()
&& !this.focus_handle.contains_focused(window, cx)
{
this.hide_scrollbar(window, cx);
cx.notify();
}
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(
// percentage as f32..end_offset as f32,
self.vertical_scrollbar.state.clone(),
))
}
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(
// percentage as f32..end_offset as f32,
self.scrollbar_state.clone(),
)),
)
/// Renders the horizontal scrollbar.
///
/// The right offset is used to determine how far to the right the
/// scrollbar should extend to, useful for ensuring it doesn't collide
/// with the vertical scrollbar when visible.
fn render_horizontal_scrollbar(
&self,
right_offset: Pixels,
cx: &mut Context<Self>,
) -> impl IntoElement {
div()
.id("git-panel-horizontal-scroll")
.occlude()
.flex_none()
.w_full()
.cursor_default()
.absolute()
.bottom_neg_px()
.left_0()
.right_0()
.pr(right_offset)
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
if !this.horizontal_scrollbar.state.is_dragging()
&& !this.focus_handle.contains_focused(window, cx)
{
this.horizontal_scrollbar.hide(window, cx);
cx.notify();
}
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.children(Scrollbar::horizontal(
// percentage as f32..end_offset as f32,
self.horizontal_scrollbar.state.clone(),
))
}
fn render_buffer_header_controls(
@ -2900,54 +3099,147 @@ impl GitPanel {
) -> impl IntoElement {
let entry_count = self.entries.len();
h_flex()
let scroll_track_size = px(16.);
let h_scroll_offset = if self.vertical_scrollbar.show_scrollbar {
// magic number
px(3.)
} else {
px(0.)
};
v_flex()
.flex_1()
.size_full()
.flex_grow()
.overflow_hidden()
.relative()
// Show a border on the top and bottom of the container when
// the vertical scrollbar container is visible so we don't have a
// floating left border in the panel.
.when(self.vertical_scrollbar.show_track, |this| {
this.border_t_1()
.border_b_1()
.border_color(cx.theme().colors().border)
})
.child(
uniform_list(cx.entity().clone(), "entries", entry_count, {
move |this, range, window, cx| {
let mut items = Vec::with_capacity(range.end - range.start);
h_flex()
.flex_1()
.size_full()
.relative()
.overflow_hidden()
.child(
uniform_list(cx.entity().clone(), "entries", entry_count, {
move |this, range, window, cx| {
let mut items = Vec::with_capacity(range.end - range.start);
for ix in range {
match &this.entries.get(ix) {
Some(GitListEntry::GitStatusEntry(entry)) => {
items.push(this.render_entry(
ix,
entry,
has_write_access,
window,
cx,
));
for ix in range {
match &this.entries.get(ix) {
Some(GitListEntry::GitStatusEntry(entry)) => {
items.push(this.render_entry(
ix,
entry,
has_write_access,
window,
cx,
));
}
Some(GitListEntry::Header(header)) => {
items.push(this.render_list_header(
ix,
header,
has_write_access,
window,
cx,
));
}
None => {}
}
}
Some(GitListEntry::Header(header)) => {
items.push(this.render_list_header(
ix,
header,
has_write_access,
window,
cx,
));
}
None => {}
items
}
}
items
}
})
.size_full()
.with_sizing_behavior(ListSizingBehavior::Auto)
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
.track_scroll(self.scroll_handle.clone()),
})
.size_full()
.flex_grow()
.with_sizing_behavior(ListSizingBehavior::Auto)
.with_horizontal_sizing_behavior(
ListHorizontalSizingBehavior::Unconstrained,
)
.with_width_from_item(self.max_width_item_index)
.track_scroll(self.scroll_handle.clone()),
)
.on_mouse_down(
MouseButton::Right,
cx.listener(move |this, event: &MouseDownEvent, window, cx| {
this.deploy_panel_context_menu(event.position, window, cx)
}),
)
.when(self.vertical_scrollbar.show_track, |this| {
this.child(
v_flex()
.h_full()
.flex_none()
.w(scroll_track_size)
.bg(cx.theme().colors().panel_background)
.child(
div()
.size_full()
.flex_1()
.border_l_1()
.border_color(cx.theme().colors().border),
),
)
})
.when(self.vertical_scrollbar.show_scrollbar, |this| {
this.child(
self.render_vertical_scrollbar(
self.horizontal_scrollbar.show_track,
cx,
),
)
}),
)
.on_mouse_down(
MouseButton::Right,
cx.listener(move |this, event: &MouseDownEvent, window, cx| {
this.deploy_panel_context_menu(event.position, window, cx)
}),
)
.children(self.render_scrollbar(cx))
.when(self.horizontal_scrollbar.show_track, |this| {
this.child(
h_flex()
.w_full()
.h(scroll_track_size)
.flex_none()
.relative()
.child(
div()
.w_full()
.flex_1()
// for some reason the horizontal scrollbar is 1px
// taller than the vertical scrollbar??
.h(scroll_track_size - px(1.))
.bg(cx.theme().colors().panel_background)
.border_t_1()
.border_color(cx.theme().colors().border),
)
.when(self.vertical_scrollbar.show_track, |this| {
this.child(
div()
.flex_none()
// -1px prevents a missing pixel between the two container borders
.w(scroll_track_size - px(1.))
.h_full(),
)
.child(
// HACK: Fill the missing 1px 🥲
div()
.absolute()
.right(scroll_track_size - px(1.))
.bottom(scroll_track_size - px(1.))
.size_px()
.bg(cx.theme().colors().border),
)
}),
)
})
.when(self.horizontal_scrollbar.show_scrollbar, |this| {
this.child(self.render_horizontal_scrollbar(h_scroll_offset, cx))
})
}
fn entry_label(&self, label: impl Into<SharedString>, color: Color) -> Label {
@ -3074,13 +3366,8 @@ impl GitPanel {
window: &Window,
cx: &Context<Self>,
) -> AnyElement {
let display_name = entry
.worktree_path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| entry.worktree_path.to_string_lossy().into_owned());
let display_name = entry.display_name();
let worktree_path = entry.worktree_path.clone();
let selected = self.selected_entry == Some(ix);
let marked = self.marked_entries.contains(&ix);
let status_style = GitPanelSettings::get_global(cx).status_style;
@ -3272,12 +3559,12 @@ impl GitPanel {
.child(
h_flex()
.items_center()
.overflow_hidden()
.when_some(worktree_path.parent(), |this, parent| {
let parent_str = parent.to_string_lossy();
if !parent_str.is_empty() {
.flex_1()
// .overflow_hidden()
.when_some(entry.parent_dir(), |this, parent| {
if !parent.is_empty() {
this.child(
self.entry_label(format!("{}/", parent_str), path_color)
self.entry_label(format!("{}/", parent), path_color)
.when(status.is_deleted(), |this| this.strikethrough()),
)
} else {
@ -3351,14 +3638,13 @@ impl Render for GitPanel {
.when(has_write_access && has_co_authors, |git_panel| {
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
})
// .on_action(cx.listener(|this, &OpenSelected, cx| this.open_selected(&OpenSelected, cx)))
.on_hover(cx.listener(|this, hovered, window, cx| {
.on_hover(cx.listener(move |this, hovered, window, cx| {
if *hovered {
this.show_scrollbar = true;
this.hide_scrollbar_task.take();
this.horizontal_scrollbar.show(cx);
this.vertical_scrollbar.show(cx);
cx.notify();
} else if !this.focus_handle.contains_focused(window, cx) {
this.hide_scrollbar(window, cx);
this.hide_scrollbars(window, cx);
}
}))
.size_full()