feat: keep track of window size and position

This commit is contained in:
Georg Bauer
2026-08-01 21:01:43 +02:00
parent c0e4ec13cc
commit 94ed73e235
3 changed files with 86 additions and 26 deletions

View File

@@ -339,8 +339,7 @@ pub(crate) enum Message {
ExportChatPicked(Option<PathBuf>, String), ExportChatPicked(Option<PathBuf>, String),
ModelManagerOpened(window::Id), ModelManagerOpened(window::Id),
WindowOpened(window::Id), WindowOpened(window::Id),
WindowCloseRequested(window::Id), WindowEvent(window::Id, window::Event),
WindowClosed(window::Id),
RequestQuit, RequestQuit,
ConfirmQuit, ConfirmQuit,
CancelQuit, CancelQuit,
@@ -1046,33 +1045,50 @@ impl App {
return focus_composer(); return focus_composer();
} }
} }
Message::WindowCloseRequested(id) => { Message::WindowEvent(id, event) => match event {
if id == self.main_window { window::Event::Opened { position, size } if id == self.main_window => {
return self.update(Message::RequestQuit); self.config.interface.window_size =
[size.width.round() as u32, size.height.round() as u32];
self.config.interface.window_position = position
.map(|position| [position.x.round() as i32, position.y.round() as i32]);
} }
return window::close(id); window::Event::Moved(position) if id == self.main_window => {
} self.config.interface.window_position =
Message::WindowClosed(id) => { Some([position.x.round() as i32, position.y.round() as i32]);
if self.model_manager_window == Some(id) {
self.model_manager_window = None;
self.pending_model_delete = None;
} }
if self.preferences_window == Some(id) { window::Event::Resized(size) if id == self.main_window => {
self.preferences_window = None; self.config.interface.window_size =
self.preference_error = None; [size.width.round() as u32, size.height.round() as u32];
self.restore_dev_brain_confirmation = false;
} }
if self.help_window == Some(id) { window::Event::CloseRequested => {
self.help_window = None; if id == self.main_window {
return self.update(Message::RequestQuit);
}
return window::close(id);
} }
} window::Event::Closed => {
if self.model_manager_window == Some(id) {
self.model_manager_window = None;
self.pending_model_delete = None;
}
if self.preferences_window == Some(id) {
self.preferences_window = None;
self.preference_error = None;
self.restore_dev_brain_confirmation = false;
}
if self.help_window == Some(id) {
self.help_window = None;
}
}
_ => {}
},
Message::RequestQuit => { Message::RequestQuit => {
if self.active_chat_count() == 0 { if self.active_chat_count() == 0 {
return iced::exit(); return self.quit();
} }
self.quit_confirmation = true; self.quit_confirmation = true;
} }
Message::ConfirmQuit => return iced::exit(), Message::ConfirmQuit => return self.quit(),
Message::CancelQuit => self.quit_confirmation = false, Message::CancelQuit => self.quit_confirmation = false,
Message::Escape(id) => { Message::Escape(id) => {
if self.preferences_window == Some(id) { if self.preferences_window == Some(id) {
@@ -1888,7 +1904,7 @@ impl App {
// focus, so a dialog would never see it through `on_key_press`. // focus, so a dialog would never see it through `on_key_press`.
iced::event::listen_with(|event, _, id| { iced::event::listen_with(|event, _, id| {
matches!( matches!(
event, &event,
iced::Event::Keyboard(keyboard::Event::KeyPressed { iced::Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::Escape), key: keyboard::Key::Named(keyboard::key::Named::Escape),
.. ..
@@ -1896,8 +1912,17 @@ impl App {
) )
.then_some(Message::Escape(id)) .then_some(Message::Escape(id))
}), }),
window::close_requests().map(Message::WindowCloseRequested), window::events().filter_map(|(id, event)| {
window::close_events().map(Message::WindowClosed), matches!(
event,
window::Event::Opened { .. }
| window::Event::Moved(_)
| window::Event::Resized(_)
| window::Event::CloseRequested
| window::Event::Closed
)
.then_some(Message::WindowEvent(id, event))
}),
]; ];
subscriptions subscriptions
.push(iced::time::every(METRICS_SAMPLE_INTERVAL).map(|_| Message::MetricsTick)); .push(iced::time::every(METRICS_SAMPLE_INTERVAL).map(|_| Message::MetricsTick));
@@ -2071,6 +2096,20 @@ impl App {
} }
} }
fn quit(&mut self) -> Task<Message> {
if self.database.is_none() {
return iced::exit();
}
match self.config.save(&config_path()) {
Ok(()) => iced::exit(),
Err(error) => {
self.quit_confirmation = false;
self.error = Some(error);
Task::none()
}
}
}
fn finish_cache_change(&mut self) { fn finish_cache_change(&mut self) {
self.metrics.rescan_cache(&kv_cache_path()); self.metrics.rescan_cache(&kv_cache_path());
self.metrics_snapshot = self.metrics.snapshot(); self.metrics_snapshot = self.metrics.snapshot();

View File

@@ -229,6 +229,8 @@ impl Default for EndpointConfig {
pub struct InterfaceConfig { pub struct InterfaceConfig {
pub sidebar_collapsed: bool, pub sidebar_collapsed: bool,
pub sidebar_width: i32, pub sidebar_width: i32,
pub window_size: [u32; 2],
pub window_position: Option<[i32; 2]>,
/// Project the app reopens on. Cleared when that project goes away. /// Project the app reopens on. Cleared when that project goes away.
pub last_project_id: Option<i32>, pub last_project_id: Option<i32>,
} }
@@ -238,6 +240,8 @@ impl Default for InterfaceConfig {
Self { Self {
sidebar_collapsed: false, sidebar_collapsed: false,
sidebar_width: 276, sidebar_width: 276,
window_size: [1120, 720],
window_position: None,
last_project_id: None, last_project_id: None,
} }
} }
@@ -348,6 +352,11 @@ mod tests {
whitespace: GitDiffWhitespace::IgnoreEndOfLine, whitespace: GitDiffWhitespace::IgnoreEndOfLine,
..GitConfig::default() ..GitConfig::default()
}, },
interface: InterfaceConfig {
window_size: [1280, 800],
window_position: Some([120, -40]),
..InterfaceConfig::default()
},
..Config::default() ..Config::default()
}; };
config.save(&path).unwrap(); config.save(&path).unwrap();
@@ -358,7 +367,8 @@ mod tests {
"model: glm-5.2\ndefault_permission_mode: ai\na2ui_enabled: false\n\ "model: glm-5.2\ndefault_permission_mode: ai\na2ui_enabled: false\n\
generation:\n context_tokens: 65536\n reasoning_mode: none\n\ generation:\n context_tokens: 65536\n reasoning_mode: none\n\
runtime:\n ssd:\n enabled: true\n cache: 64GB\n\ runtime:\n ssd:\n enabled: true\n cache: 64GB\n\
git:\n diff_layout: split\n diff_algorithm: patience\n context_lines: 5\n whitespace: ignore-end-of-line\n" git:\n diff_layout: split\n diff_algorithm: patience\n context_lines: 5\n whitespace: ignore-end-of-line\n\
interface:\n window_size:\n - 1280\n - 800\n window_position:\n - 120\n - -40\n"
); );
assert_eq!(Config::load(&path).unwrap(), config); assert_eq!(Config::load(&path).unwrap(), config);
fs::remove_dir_all(&directory).unwrap(); fs::remove_dir_all(&directory).unwrap();

View File

@@ -26,7 +26,7 @@ mod server;
mod settings; mod settings;
use app::{App, Message, app_icon, app_theme}; use app::{App, Message, app_icon, app_theme};
use iced::{Size, window}; use iced::{Point, Size, window};
fn main() -> iced::Result { fn main() -> iced::Result {
if std::env::args().nth(1).as_deref() == Some("validate-a2ui") { if std::env::args().nth(1).as_deref() == Some("validate-a2ui") {
@@ -42,8 +42,19 @@ fn main() -> iced::Result {
} }
iced::daemon( iced::daemon(
|| { || {
let interface = config::Config::load(&app::config_path())
.unwrap_or_default()
.interface;
let (main_window, open) = window::open(window::Settings { let (main_window, open) = window::open(window::Settings {
size: Size::new(1120.0, 720.0), size: Size::new(
interface.window_size[0] as f32,
interface.window_size[1] as f32,
),
position: interface
.window_position
.map_or(window::Position::Default, |[x, y]| {
window::Position::Specific(Point::new(x as f32, y as f32))
}),
min_size: Some(Size::new(760.0, 480.0)), min_size: Some(Size::new(760.0, 480.0)),
icon: Some(app_icon()), icon: Some(app_icon()),
exit_on_close_request: false, exit_on_close_request: false,