Add pane resizing
This commit is contained in:
@@ -60,9 +60,30 @@ pub fn LibTab() -> impl IntoView {
|
|||||||
.map(|sq| sq.0)
|
.map(|sq| sq.0)
|
||||||
.unwrap_or_else(|| RwSignal::new(String::new()));
|
.unwrap_or_else(|| RwSignal::new(String::new()));
|
||||||
|
|
||||||
|
let layout_config = use_context::<crate::LayoutContext>()
|
||||||
|
.expect("LayoutContext missing")
|
||||||
|
.0;
|
||||||
|
|
||||||
// ── UI state ──────────────────────────────────────────────────────────────
|
// ── UI state ──────────────────────────────────────────────────────────────
|
||||||
let focused = RwSignal::new(Pane::Tree);
|
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
|
// Tree state
|
||||||
let root_libs = RwSignal::new(Vec::<Library>::new());
|
let root_libs = RwSignal::new(Vec::<Library>::new());
|
||||||
let children_cache = RwSignal::new(HashMap::<String, Vec<Library>>::new());
|
let children_cache = RwSignal::new(HashMap::<String, Vec<Library>>::new());
|
||||||
@@ -76,6 +97,38 @@ pub fn LibTab() -> impl IntoView {
|
|||||||
// Detail state
|
// Detail state
|
||||||
let detail_ref = RwSignal::new(Option::<Reference>::None);
|
let detail_ref = RwSignal::new(Option::<Reference>::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 ────────────────────────────────────────────────────
|
// ── Derived / computed ────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Flattened visible tree rows (recomputed when tree data or expand set changes).
|
// Flattened visible tree rows (recomputed when tree data or expand set changes).
|
||||||
@@ -182,7 +235,10 @@ pub fn LibTab() -> impl IntoView {
|
|||||||
// ── View ──────────────────────────────────────────────────────────────────
|
// ── View ──────────────────────────────────────────────────────────────────
|
||||||
view! {
|
view! {
|
||||||
<div class="lib-tab">
|
<div class="lib-tab">
|
||||||
<div class="pane pane-left">
|
<div
|
||||||
|
class="pane pane-left"
|
||||||
|
style=move || format!("width: {}px", left_width.get())
|
||||||
|
>
|
||||||
<LibraryTree
|
<LibraryTree
|
||||||
root_libs=root_libs
|
root_libs=root_libs
|
||||||
children_cache=children_cache
|
children_cache=children_cache
|
||||||
@@ -191,6 +247,16 @@ pub fn LibTab() -> impl IntoView {
|
|||||||
focused=focused
|
focused=focused
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="resizer"
|
||||||
|
class:resizing=move || resizing_left.get()
|
||||||
|
on:mousedown=move |ev| {
|
||||||
|
ev.prevent_default();
|
||||||
|
resizing_left.set(true);
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="pane pane-center">
|
<div class="pane pane-center">
|
||||||
<PubList
|
<PubList
|
||||||
items=list_items
|
items=list_items
|
||||||
@@ -198,7 +264,21 @@ pub fn LibTab() -> impl IntoView {
|
|||||||
focused=focused
|
focused=focused
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="pane pane-right" class:pane-focused=move || focused.get() == Pane::Detail>
|
|
||||||
|
<div
|
||||||
|
class="resizer"
|
||||||
|
class:resizing=move || resizing_right.get()
|
||||||
|
on:mousedown=move |ev| {
|
||||||
|
ev.prevent_default();
|
||||||
|
resizing_right.set(true);
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="pane pane-right"
|
||||||
|
class:pane-focused=move || focused.get() == Pane::Detail
|
||||||
|
style=move || format!("width: {}px", right_width.get())
|
||||||
|
>
|
||||||
<PubDetail reference=detail_ref />
|
<PubDetail reference=detail_ref />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -373,12 +373,39 @@ fn provide_tabs() -> (RwSignal<Vec<AppTab>>, RwSignal<usize>) {
|
|||||||
(tabs, active_tab)
|
(tabs, active_tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Layout context ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Context handle for the current layout configuration.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct LayoutContext(pub ReadSignal<crate::tauri::LayoutConfig>);
|
||||||
|
|
||||||
|
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 ─────────────────────────────────────────────────────────────
|
// ── Root component ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn App() -> impl IntoView {
|
fn App() -> impl IntoView {
|
||||||
provide_keymap();
|
provide_keymap();
|
||||||
provide_theme();
|
provide_theme();
|
||||||
|
provide_layout();
|
||||||
let mode = provide_mode();
|
let mode = provide_mode();
|
||||||
provide_search_query();
|
provide_search_query();
|
||||||
let (tabs, active_tab) = provide_tabs();
|
let (tabs, active_tab) = provide_tabs();
|
||||||
|
|||||||
@@ -56,6 +56,18 @@ async fn invoke<T: for<'de> 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 ────────────────────────────────────────────────────────────
|
// ── Config commands ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Return the last-opened project path from the global config, or `None` if
|
/// Return the last-opened project path from the global config, or `None` if
|
||||||
@@ -64,6 +76,11 @@ pub async fn get_last_project() -> Result<Option<String>, String> {
|
|||||||
invoke("get_last_project", &NoArgs {}).await
|
invoke("get_last_project", &NoArgs {}).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the user's layout configuration from the global config.
|
||||||
|
pub async fn get_layout_config() -> Result<LayoutConfig, String> {
|
||||||
|
invoke("get_layout_config", &NoArgs {}).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the user's keybinding overrides from `~/.config/brittle/config.toml`.
|
/// Return the user's keybinding overrides from `~/.config/brittle/config.toml`.
|
||||||
///
|
///
|
||||||
/// Keys are snake_case action names (e.g. `"tab_next"`); values are
|
/// Keys are snake_case action names (e.g. `"tab_next"`); values are
|
||||||
|
|||||||
@@ -117,18 +117,38 @@ ul { list-style: none; }
|
|||||||
height: 100%;
|
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 {
|
.pane {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane:last-child { border-right: none; }
|
.pane-left { flex-shrink: 0; }
|
||||||
|
|
||||||
.pane-left { width: 220px; flex-shrink: 0; }
|
|
||||||
.pane-center { flex: 1; min-width: 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; }
|
.pane-focused { outline: 1px solid var(--focused-ring); outline-offset: -1px; }
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,14 @@ pub fn get_last_project() -> Result<Option<String>, String> {
|
|||||||
.map(|p| p.to_string_lossy().into_owned()))
|
.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<crate::config::LayoutConfig, String> {
|
||||||
|
GlobalConfig::load()
|
||||||
|
.map(|c| c.layout)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the user's keybinding overrides from the global config.
|
/// Return the user's keybinding overrides from the global config.
|
||||||
///
|
///
|
||||||
/// Map keys are action names in snake_case (e.g. `"tab_next"`); values are
|
/// Map keys are action names in snake_case (e.g. `"tab_next"`); values are
|
||||||
|
|||||||
@@ -53,12 +53,24 @@ pub struct AppearanceOverride {
|
|||||||
pub font_size: Option<u32>,
|
pub font_size: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pane layout proportions (fractions of the window width, 0..1).
|
/// Pane layout proportions and constraints.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct LayoutConfig {
|
pub struct LayoutConfig {
|
||||||
|
/// Initial fraction of the window width for the left pane (0..1).
|
||||||
pub left_pane_fraction: f32,
|
pub left_pane_fraction: f32,
|
||||||
|
/// Initial fraction of the window width for the right pane (0..1).
|
||||||
pub right_pane_fraction: f32,
|
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 {
|
impl Default for LayoutConfig {
|
||||||
@@ -66,6 +78,11 @@ impl Default for LayoutConfig {
|
|||||||
Self {
|
Self {
|
||||||
left_pane_fraction: 0.20,
|
left_pane_fraction: 0.20,
|
||||||
right_pane_fraction: 0.35,
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub fn run() {
|
|||||||
commands::config::get_theme,
|
commands::config::get_theme,
|
||||||
commands::config::set_theme,
|
commands::config::set_theme,
|
||||||
commands::config::get_keybindings,
|
commands::config::get_keybindings,
|
||||||
|
commands::config::get_layout_config,
|
||||||
commands::config::get_last_project,
|
commands::config::get_last_project,
|
||||||
// repository
|
// repository
|
||||||
commands::repository::create_repository,
|
commands::repository::create_repository,
|
||||||
|
|||||||
Reference in New Issue
Block a user