From 270c8adc11be3f1c2535e8cf25335aa4d85bca9e Mon Sep 17 00:00:00 2001 From: Andreas Tsouchlos Date: Fri, 27 Mar 2026 16:20:05 +0100 Subject: [PATCH] Add pane resizing --- brittle-ui/src/lib_tab.rs | 84 +++++++++++++++++++++++++++++++- brittle-ui/src/main.rs | 27 ++++++++++ brittle-ui/src/tauri.rs | 17 +++++++ brittle-ui/style.css | 30 ++++++++++-- src-tauri/src/commands/config.rs | 8 +++ src-tauri/src/config/mod.rs | 19 +++++++- src-tauri/src/lib.rs | 1 + 7 files changed, 178 insertions(+), 8 deletions(-) diff --git a/brittle-ui/src/lib_tab.rs b/brittle-ui/src/lib_tab.rs index e4a98d9..b216e35 100644 --- a/brittle-ui/src/lib_tab.rs +++ b/brittle-ui/src/lib_tab.rs @@ -60,9 +60,30 @@ pub fn LibTab() -> impl IntoView { .map(|sq| sq.0) .unwrap_or_else(|| RwSignal::new(String::new())); + let layout_config = use_context::() + .expect("LayoutContext missing") + .0; + // ── UI state ────────────────────────────────────────────────────────────── let focused = RwSignal::new(Pane::Tree); + // Resizing state + let left_width = RwSignal::new(220); + let right_width = RwSignal::new(300); + let resizing_left = RwSignal::new(false); + let resizing_right = RwSignal::new(false); + + // Initial widths from layout config (fractions) + Effect::new(move |_| { + let config = layout_config.get(); + if let Some(win) = web_sys::window() { + if let Ok(Some(total_width)) = win.inner_width().map(|v| v.as_f64()) { + left_width.set((total_width * config.left_pane_fraction as f64) as i32); + right_width.set((total_width * config.right_pane_fraction as f64) as i32); + } + } + }); + // Tree state let root_libs = RwSignal::new(Vec::::new()); let children_cache = RwSignal::new(HashMap::>::new()); @@ -76,6 +97,38 @@ pub fn LibTab() -> impl IntoView { // Detail state let detail_ref = RwSignal::new(Option::::None); + // ── Resizing Logic ──────────────────────────────────────────────────────── + let on_mousemove = window_event_listener(leptos::ev::mousemove, move |ev| { + if let Some(win) = web_sys::window() { + if let Ok(Some(total_width)) = win.inner_width().map(|v| v.as_f64()) { + let total_width = total_width as i32; + let config = layout_config.get_untracked(); + + if resizing_left.get_untracked() { + let new_width = ev.client_x(); + // Constraints: min, max, AND leave enough space for center + let max_allowed = (total_width - right_width.get_untracked() - config.center_pane_min).max(config.left_pane_min); + left_width.set(new_width.max(config.left_pane_min).min(config.left_pane_max).min(max_allowed)); + } else if resizing_right.get_untracked() { + let new_width = total_width - ev.client_x(); + // Constraints: min, max, AND leave enough space for center + let max_allowed = (total_width - left_width.get_untracked() - config.center_pane_min).max(config.right_pane_min); + right_width.set(new_width.max(config.right_pane_min).min(config.right_pane_max).min(max_allowed)); + } + } + } + }); + + let on_mouseup = window_event_listener(leptos::ev::mouseup, move |_| { + resizing_left.set(false); + resizing_right.set(false); + }); + + on_cleanup(move || { + on_mousemove.remove(); + on_mouseup.remove(); + }); + // ── Derived / computed ──────────────────────────────────────────────────── // Flattened visible tree rows (recomputed when tree data or expand set changes). @@ -182,7 +235,10 @@ pub fn LibTab() -> impl IntoView { // ── View ────────────────────────────────────────────────────────────────── view! {
-
+
impl IntoView { focused=focused />
+ +
+
impl IntoView { focused=focused />
-
+ +
+ +
diff --git a/brittle-ui/src/main.rs b/brittle-ui/src/main.rs index 8e09d94..c2b99f4 100644 --- a/brittle-ui/src/main.rs +++ b/brittle-ui/src/main.rs @@ -373,12 +373,39 @@ fn provide_tabs() -> (RwSignal>, RwSignal) { (tabs, active_tab) } +// ── Layout context ──────────────────────────────────────────────────────────── + +/// Context handle for the current layout configuration. +#[derive(Clone, Copy)] +pub struct LayoutContext(pub ReadSignal); + +fn provide_layout() { + let (layout, set_layout) = signal(crate::tauri::LayoutConfig { + left_pane_fraction: 0.20, + right_pane_fraction: 0.35, + left_pane_min: 120, + left_pane_max: 600, + right_pane_min: 150, + right_pane_max: 600, + center_pane_min: 200, + }); + provide_context(LayoutContext(layout)); + + // Load layout config from backend. + leptos::task::spawn_local(async move { + if let Ok(l) = crate::tauri::get_layout_config().await { + set_layout.set(l); + } + }); +} + // ── Root component ───────────────────────────────────────────────────────────── #[component] fn App() -> impl IntoView { provide_keymap(); provide_theme(); + provide_layout(); let mode = provide_mode(); provide_search_query(); let (tabs, active_tab) = provide_tabs(); diff --git a/brittle-ui/src/tauri.rs b/brittle-ui/src/tauri.rs index cade31c..b8e126b 100644 --- a/brittle-ui/src/tauri.rs +++ b/brittle-ui/src/tauri.rs @@ -56,6 +56,18 @@ async fn invoke serde::Deserialize<'de>, A: Serialize>( } } +#[derive(serde::Deserialize, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub struct LayoutConfig { + pub left_pane_fraction: f32, + pub right_pane_fraction: f32, + pub left_pane_min: i32, + pub left_pane_max: i32, + pub right_pane_min: i32, + pub right_pane_max: i32, + pub center_pane_min: i32, +} + // ── Config commands ──────────────────────────────────────────────────────────── /// Return the last-opened project path from the global config, or `None` if @@ -64,6 +76,11 @@ pub async fn get_last_project() -> Result, String> { invoke("get_last_project", &NoArgs {}).await } +/// Return the user's layout configuration from the global config. +pub async fn get_layout_config() -> Result { + invoke("get_layout_config", &NoArgs {}).await +} + /// Return the user's keybinding overrides from `~/.config/brittle/config.toml`. /// /// Keys are snake_case action names (e.g. `"tab_next"`); values are diff --git a/brittle-ui/style.css b/brittle-ui/style.css index 82fead7..873dcdc 100644 --- a/brittle-ui/style.css +++ b/brittle-ui/style.css @@ -117,18 +117,38 @@ ul { list-style: none; } height: 100%; } +.resizer { + width: 2px; + background: var(--border); + cursor: col-resize; + flex-shrink: 0; + transition: background 0.2s; + z-index: 10; + position: relative; +} + +.resizer::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: -4px; + right: -4px; +} + +.resizer:hover, .resizer.resizing { + background: var(--accent); +} + .pane { display: flex; flex-direction: column; overflow: hidden; - border-right: 1px solid var(--border); } -.pane:last-child { border-right: none; } - -.pane-left { width: 220px; flex-shrink: 0; } +.pane-left { flex-shrink: 0; } .pane-center { flex: 1; min-width: 0; } -.pane-right { width: 300px; flex-shrink: 0; } +.pane-right { flex-shrink: 0; } .pane-focused { outline: 1px solid var(--focused-ring); outline-offset: -1px; } diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 2c64f91..ec54eff 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -72,6 +72,14 @@ pub fn get_last_project() -> Result, String> { .map(|p| p.to_string_lossy().into_owned())) } +/// Return the user's layout configuration from the global config. +#[tauri::command] +pub fn get_layout_config() -> Result { + GlobalConfig::load() + .map(|c| c.layout) + .map_err(|e| e.to_string()) +} + /// Return the user's keybinding overrides from the global config. /// /// Map keys are action names in snake_case (e.g. `"tab_next"`); values are diff --git a/src-tauri/src/config/mod.rs b/src-tauri/src/config/mod.rs index 38a1c69..f4c213b 100644 --- a/src-tauri/src/config/mod.rs +++ b/src-tauri/src/config/mod.rs @@ -53,12 +53,24 @@ pub struct AppearanceOverride { pub font_size: Option, } -/// Pane layout proportions (fractions of the window width, 0..1). +/// Pane layout proportions and constraints. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct LayoutConfig { + /// Initial fraction of the window width for the left pane (0..1). pub left_pane_fraction: f32, + /// Initial fraction of the window width for the right pane (0..1). pub right_pane_fraction: f32, + /// Minimum width of the left pane in pixels. + pub left_pane_min: i32, + /// Maximum width of the left pane in pixels. + pub left_pane_max: i32, + /// Minimum width of the right pane in pixels. + pub right_pane_min: i32, + /// Maximum width of the right pane in pixels. + pub right_pane_max: i32, + /// Minimum width of the center pane in pixels. + pub center_pane_min: i32, } impl Default for LayoutConfig { @@ -66,6 +78,11 @@ impl Default for LayoutConfig { Self { left_pane_fraction: 0.20, right_pane_fraction: 0.35, + left_pane_min: 120, + left_pane_max: 600, + right_pane_min: 150, + right_pane_max: 600, + center_pane_min: 200, } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 64d9c2a..add79c0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -30,6 +30,7 @@ pub fn run() { commands::config::get_theme, commands::config::set_theme, commands::config::get_keybindings, + commands::config::get_layout_config, commands::config::get_last_project, // repository commands::repository::create_repository,