diff --git a/Cargo.lock b/Cargo.lock index a5ea621cd14662cba0838558b70d3e13b51c7840..6b7f2f5888473aed073e0be77b7a6ddde87a37a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14756,6 +14756,7 @@ dependencies = [ "fs", "fuzzy", "gpui", + "itertools 0.14.0", "language", "log", "menu", diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 651397dd51b1b2406cc4149f0951d3f506b73689..02327045fdb2279597342a5d838d356d7b738c73 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -23,6 +23,7 @@ feature_flags.workspace = true fs.workspace = true fuzzy.workspace = true gpui.workspace = true +itertools.workspace = true language.workspace = true log.workspace = true menu.workspace = true diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 5f940e8a25cc784a5ad0bd529e213c2e0b69a03d..1ad940e00ed0dcc9d2adbfc19f846c34f3fe1957 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1594,15 +1594,14 @@ impl Render for KeymapEditor { .collect() }), ) - .map_row( - cx.processor(|this, (row_index, row): (usize, Div), _window, cx| { + .map_row(cx.processor( + |this, (row_index, row): (usize, Stateful
), _window, cx| { let is_conflict = this.has_conflict(row_index); let is_selected = this.selected_index == Some(row_index); let row_id = row_group_id(row_index); let row = row - .id(row_id.clone()) .on_any_mouse_down(cx.listener( move |this, mouse_down_event: &gpui::MouseDownEvent, @@ -1636,11 +1635,12 @@ impl Render for KeymapEditor { }) .when(is_selected, |row| { row.border_color(cx.theme().colors().panel_focused_border) + .border_2() }); row.into_any_element() - }), - ), + }, + )), ) .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _, cx| { // This ensures that the menu is not dismissed in cases where scroll events diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 6ea59cd2f42eb570237465430ccffbf8f753b16f..eaf45f05cf5274cf6aad1d419f6db7385a67b30c 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -3,14 +3,16 @@ use std::{ops::Range, rc::Rc, time::Duration}; use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide}; use gpui::{ AppContext, Axis, Context, Entity, FocusHandle, Length, ListHorizontalSizingBehavior, - ListSizingBehavior, MouseButton, Point, Task, UniformListScrollHandle, WeakEntity, + ListSizingBehavior, MouseButton, Point, Stateful, Task, UniformListScrollHandle, WeakEntity, transparent_black, uniform_list, }; + +use itertools::intersperse_with; use settings::Settings as _; use ui::{ ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, - InteractiveElement as _, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, + InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, Scrollbar, ScrollbarState, StatefulInteractiveElement as _, Styled, StyledExt as _, StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex, }; @@ -191,6 +193,67 @@ impl TableInteractionState { } } + fn render_resize_handles( + &self, + column_widths: &[Length; COLS], + window: &mut Window, + cx: &mut App, + ) -> AnyElement { + let spacers = column_widths + .iter() + .map(|width| base_cell_style(Some(*width))); + + let mut column_ix = 0; + let dividers = intersperse_with(spacers, || { + let hovered = + window + .use_keyed_state(("resize-hover", column_ix as u32), cx, |_window, _cx| false); + + let div = div() + .relative() + .top_0() + .w_0p5() + .h_full() + .bg(cx.theme().colors().border_variant.opacity(0.5)) + .when(*hovered.read(cx), |div| { + div.bg(cx.theme().colors().border_focused) + }) + .child( + div() + .id(("column-resize-handle", column_ix as u32)) + .absolute() + .left_neg_0p5() + .w_1p5() + .h_full() + .on_hover(move |&was_hovered, _, cx| { + hovered.update(cx, |hovered, _| { + *hovered = was_hovered; + }) + }) + .cursor_col_resize() + .on_mouse_down(MouseButton::Left, { + let column_idx = column_ix; + move |_event, _window, _cx| { + // TODO: Emit resize event to parent + eprintln!("Start resizing column {}", column_idx); + } + }), + ); + + column_ix += 1; + div + }); + + div() + .id("id") + .h_flex() + .absolute() + .w_full() + .inset_0() + .children(dividers) + .into_any_element() + } + fn render_vertical_scrollbar_track( this: &Entity, parent: Div, @@ -385,7 +448,7 @@ pub struct Table { impl Table { /// number of headers provided. pub fn new() -> Self { - Table { + Self { striped: false, width: None, headers: None, @@ -459,9 +522,14 @@ impl Table { self } + pub fn resizable_columns(mut self) -> Self { + self.resizable_columns = true; + self + } + pub fn map_row( mut self, - callback: impl Fn((usize, Div), &mut Window, &mut App) -> AnyElement + 'static, + callback: impl Fn((usize, Stateful
), &mut Window, &mut App) -> AnyElement + 'static, ) -> Self { self.map_row = Some(Rc::new(callback)); self @@ -477,18 +545,21 @@ impl Table { } } -fn base_cell_style(width: Option, cx: &App) -> Div { +fn base_cell_style(width: Option) -> Div { div() .px_1p5() .when_some(width, |this, width| this.w(width)) .when(width.is_none(), |this| this.flex_1()) .justify_start() - .text_ui(cx) .whitespace_nowrap() .text_ellipsis() .overflow_hidden() } +fn base_cell_style_text(width: Option, cx: &App) -> Div { + base_cell_style(width).text_ui(cx) +} + pub fn render_row( row_index: usize, items: [impl IntoElement; COLS], @@ -507,33 +578,33 @@ pub fn render_row( .column_widths .map_or([None; COLS], |widths| widths.map(Some)); - let row = div().w_full().child( - h_flex() - .id("table_row") - .w_full() - .justify_between() - .px_1p5() - .py_1() - .when_some(bg, |row, bg| row.bg(bg)) - .when(!is_striped, |row| { - row.border_b_1() - .border_color(transparent_black()) - .when(!is_last, |row| row.border_color(cx.theme().colors().border)) - }) - .children( - items - .map(IntoElement::into_any_element) - .into_iter() - .zip(column_widths) - .map(|(cell, width)| base_cell_style(width, cx).child(cell)), - ), + let mut row = h_flex() + .h_full() + .id(("table_row", row_index)) + .w_full() + .justify_between() + .when_some(bg, |row, bg| row.bg(bg)) + .when(!is_striped, |row| { + row.border_b_1() + .border_color(transparent_black()) + .when(!is_last, |row| row.border_color(cx.theme().colors().border)) + }); + + row = row.children( + items + .map(IntoElement::into_any_element) + .into_iter() + .zip(column_widths) + .map(|(cell, width)| base_cell_style_text(width, cx).px_1p5().py_1().child(cell)), ); - if let Some(map_row) = table_context.map_row { + let row = if let Some(map_row) = table_context.map_row { map_row((row_index, row), window, cx) } else { row.into_any_element() - } + }; + + div().h_full().w_full().child(row).into_any_element() } pub fn render_header( @@ -557,7 +628,7 @@ pub fn render_header( headers .into_iter() .zip(column_widths) - .map(|(h, width)| base_cell_style(width, cx).child(h)), + .map(|(h, width)| base_cell_style_text(width, cx).child(h)), ) } @@ -566,7 +637,7 @@ pub struct TableRenderContext { pub striped: bool, pub total_row_count: usize, pub column_widths: Option<[Length; COLS]>, - pub map_row: Option AnyElement>>, + pub map_row: Option), &mut Window, &mut App) -> AnyElement>>, } impl TableRenderContext { @@ -660,6 +731,17 @@ impl RenderOnce for Table { ), ), }) + .when_some( + self.column_widths + .as_ref() + .zip(interaction_state.as_ref()) + .filter(|_| self.resizable_columns), + |parent, (column_widths, state)| { + parent.child(state.update(cx, |state, cx| { + state.render_resize_handles(column_widths, window, cx) + })) + }, + ) .when_some(interaction_state.as_ref(), |this, interaction_state| { this.map(|this| { TableInteractionState::render_vertical_scrollbar_track(