Let the sidebar divider be dragged

This commit is contained in:
Georg Bauer
2026-07-26 00:12:04 +02:00
parent 9bd648d77b
commit b182e7007c
6 changed files with 82 additions and 5 deletions

View File

@@ -25,7 +25,7 @@ use crate::settings::{
StreamingCacheBudget,
};
use iced::widget::{markdown, scrollable, text_input};
use iced::{Size, Subscription, Task, keyboard, window};
use iced::{Size, Subscription, Task, keyboard, mouse, window};
use rfd::AsyncFileDialog;
use std::collections::{HashMap, HashSet, VecDeque};
use std::fs;
@@ -38,6 +38,8 @@ use std::time::{Duration, Instant};
const APP_ID: &str = "de.rfc1437.ds4server";
const METRICS_SAMPLE_INTERVAL: Duration = Duration::from_millis(200);
pub(super) const MIN_SIDEBAR_WIDTH: i32 = 180;
pub(super) const MAX_SIDEBAR_WIDTH: i32 = 520;
pub(crate) struct App {
main_window: window::Id,
@@ -64,6 +66,8 @@ pub(crate) struct App {
session_rename: Option<(i32, String)>,
/// Projects whose archived sessions are expanded in the sidebar.
expanded_archives: HashSet<i32>,
/// True while the sidebar divider is being dragged.
sidebar_drag: bool,
choosing_folder: bool,
pending_project_path: Option<PathBuf>,
project_name_input: String,
@@ -194,6 +198,9 @@ pub(crate) enum Message {
ShowChat,
ShowStats,
ToggleSidebar,
StartSidebarDrag,
DragSidebar(f32),
EndSidebarDrag,
MetricsTick,
}
@@ -242,6 +249,7 @@ impl App {
session_menu: None,
session_rename: None,
expanded_archives: HashSet::new(),
sidebar_drag: false,
choosing_folder: false,
pending_project_path: None,
project_name_input: String::new(),
@@ -323,6 +331,7 @@ impl App {
session_menu: None,
session_rename: None,
expanded_archives: HashSet::new(),
sidebar_drag: false,
choosing_folder: false,
pending_project_path: None,
project_name_input: String::new(),
@@ -414,6 +423,26 @@ impl App {
}
Message::ShowChat => self.detail_tab = DetailTab::Chat,
Message::ShowStats => self.detail_tab = DetailTab::Stats,
// The sidebar starts at the window's left edge, so the cursor's x is
// the width the user is asking for. Only the final width is stored.
Message::StartSidebarDrag => self.sidebar_drag = true,
Message::DragSidebar(x) => {
if self.sidebar_drag {
self.preferences.sidebar_width =
(x.round() as i32).clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH);
}
}
Message::EndSidebarDrag => {
if self.sidebar_drag {
self.sidebar_drag = false;
if let Some(database) = self.database.as_mut()
&& let Err(error) =
database.set_sidebar_width(self.preferences.sidebar_width)
{
self.error = Some(error);
}
}
}
Message::ToggleSidebar => {
let collapsed = !self.preferences.sidebar_collapsed;
self.preferences.sidebar_collapsed = collapsed;
@@ -969,6 +998,19 @@ impl App {
iced::time::every(Duration::from_millis(50)).map(|_| Message::GenerationTick),
);
}
// The cursor leaves the thin divider as soon as it moves, so the drag is
// followed through raw window events instead of the widget.
if self.sidebar_drag {
subscriptions.push(iced::event::listen_with(|event, _, _| match event {
iced::Event::Mouse(mouse::Event::CursorMoved { position }) => {
Some(Message::DragSidebar(position.x))
}
iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
Some(Message::EndSidebarDrag)
}
_ => None,
}));
}
Subscription::batch(subscriptions)
}

View File

@@ -6,8 +6,8 @@ mod stats;
use model_manager::{download_status_bar, format_bytes, format_duration};
use super::{
ActiveDownload, App, DetailTab, Message, MetricsPoint, ModelDownload, ModelOperation,
chat_scroll_id, composer_id, models_path,
ActiveDownload, App, DetailTab, MAX_SIDEBAR_WIDTH, MIN_SIDEBAR_WIDTH, Message, MetricsPoint,
ModelDownload, ModelOperation, chat_scroll_id, composer_id, models_path,
};
use crate::database::{ProjectWithSessions, Session, SessionState};
use crate::model::{
@@ -17,7 +17,8 @@ use crate::settings::{GIB, REASONING_MODES};
use iced::theme::{Palette, palette};
use iced::widget::{
Button, Space, Svg, Tooltip, button, checkbox, column, container, horizontal_rule, markdown,
opaque, pick_list, progress_bar, row, scrollable, stack, svg, text, text_input, tooltip,
mouse_area, opaque, pick_list, progress_bar, row, scrollable, stack, svg, text, text_input,
tooltip,
};
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme, window};
use std::collections::VecDeque;
@@ -65,6 +66,17 @@ impl App {
let mut body = row![].width(Length::Fill).height(Length::Fill);
if !self.preferences.sidebar_collapsed {
body = body.push(self.sidebar());
body = body.push(
mouse_area(
container(Space::with_width(Length::Fill))
.width(5)
.height(Length::Fill)
.style(divider_style),
)
.interaction(iced::mouse::Interaction::ResizingHorizontally)
.on_press(Message::StartSidebarDrag)
.on_release(Message::EndSidebarDrag),
);
}
body = body.push(
container(self.detail())
@@ -259,7 +271,11 @@ impl App {
]
.spacing(10),
)
.width(276)
.width(
self.preferences
.sidebar_width
.clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH) as f32,
)
.height(Length::Fill)
.padding(Padding::new(16.0).top(16.0 + TITLE_BAR_HEIGHT))
.style(sidebar_style)
@@ -808,6 +824,10 @@ fn muted_text() -> Color {
Color::from_rgb8(174, 174, 178)
}
fn divider_style(_: &Theme) -> container::Style {
container::Style::default().background(Color::from_rgb8(38, 38, 40))
}
fn sidebar_style(_: &Theme) -> container::Style {
container::Style::default().background(Color::from_rgb8(23, 23, 25))
}

View File

@@ -57,6 +57,7 @@ pub struct AppPreferences {
pub sidebar_collapsed: bool,
/// Project the app reopens on. Cleared when that project goes away.
pub last_project_id: Option<i32>,
pub sidebar_width: i32,
}
impl Default for AppPreferences {
@@ -101,6 +102,7 @@ impl Default for AppPreferences {
endpoint_cors: false,
sidebar_collapsed: false,
last_project_id: None,
sidebar_width: 276,
}
}
}
@@ -446,6 +448,14 @@ impl Database {
.map_err(|error| error.to_string())
}
pub fn set_sidebar_width(&mut self, width: i32) -> Result<(), String> {
diesel::update(preferences::table.find(1))
.set(preferences::sidebar_width.eq(width))
.execute(&mut self.connection)
.map(|_| ())
.map_err(|error| error.to_string())
}
pub fn set_last_project(&mut self, project_id: Option<i32>) -> Result<(), String> {
diesel::update(preferences::table.find(1))
.set(preferences::last_project_id.eq(project_id))
@@ -877,6 +887,7 @@ mod tests {
database.set_sidebar_collapsed(true).unwrap();
database.set_last_project(Some(project.id)).unwrap();
database.set_sidebar_width(320).unwrap();
assert!(!loaded[0].project.collapsed);
database.set_project_collapsed(project.id, true).unwrap();
assert!(database.load_projects().unwrap()[0].project.collapsed);
@@ -889,6 +900,7 @@ mod tests {
let preferences = reopened.load_preferences().unwrap();
assert!(preferences.sidebar_collapsed);
assert_eq!(preferences.last_project_id, Some(project.id));
assert_eq!(preferences.sidebar_width, 320);
assert_eq!(preferences.selected_model, "glm-5.2");
assert_eq!(preferences.idle_timeout_minutes, 30);
assert_eq!(preferences.endpoint_port, 4567);

View File

@@ -60,6 +60,7 @@ diesel::table! {
endpoint_cors -> Bool,
sidebar_collapsed -> Bool,
last_project_id -> Nullable<Integer>,
sidebar_width -> Integer,
}
}