Skip to content

Commit a7c31e7

Browse files
Zuka MargvelashviliZuka Margvelashvili
authored andcommitted
Add snap corner preference and cut v0.2.3
1 parent da76807 commit a7c31e7

File tree

11 files changed

+159
-30
lines changed

11 files changed

+159
-30
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 0.2.3 - 2026-03-10
4+
5+
- Added a default-on setting to remove rounded corners while windows are snapped, matching the PowerToys approach of restoring the original corner preference when the window is unsnapped.
6+
- Changed the default window gap from `-2` px to `-1` px.
7+
38
## 0.2.2 - 2026-03-07
49

510
- Fixed shell hook handling so Start menu and other shell UI are no longer affected by stale snap state or invalid foreground targets.

app/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "app",
33
"private": true,
4-
"version": "0.2.2",
4+
"version": "0.2.3",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

app/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "app"
3-
version = "0.2.2"
3+
version = "0.2.3"
44
description = "RectangleWin - window snapping with keyboard shortcuts"
55
authors = ["you"]
66
edition = "2021"

app/src-tauri/src/config.rs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pub struct HotkeyBinding {
2121
pub struct PersistedConfig {
2222
#[serde(default = "default_launch_on_login")]
2323
pub launch_on_login: bool,
24+
#[serde(default = "default_true")]
25+
pub remove_rounded_corners_on_snap: bool,
2426
#[serde(default)]
2527
pub gap_size: f32,
2628
#[serde(default)]
@@ -74,6 +76,7 @@ pub struct PersistedHotkey {
7476
#[derive(Clone, Debug)]
7577
pub struct Config {
7678
pub launch_on_login: bool,
79+
pub remove_rounded_corners_on_snap: bool,
7780
pub gap_size: f32,
7881
pub screen_edge_gap_top: f32,
7982
pub screen_edge_gap_bottom: f32,
@@ -94,6 +97,7 @@ pub struct Config {
9497
#[serde(rename_all = "camelCase")]
9598
pub struct ConfigForFrontend {
9699
pub launch_on_login: bool,
100+
pub remove_rounded_corners_on_snap: bool,
97101
pub gap_size: f32,
98102
pub screen_edge_gap_top: f32,
99103
pub screen_edge_gap_bottom: f32,
@@ -120,6 +124,7 @@ impl Config {
120124
pub fn to_frontend(&self) -> ConfigForFrontend {
121125
ConfigForFrontend {
122126
launch_on_login: self.launch_on_login,
127+
remove_rounded_corners_on_snap: self.remove_rounded_corners_on_snap,
123128
gap_size: self.gap_size,
124129
screen_edge_gap_top: self.screen_edge_gap_top,
125130
screen_edge_gap_bottom: self.screen_edge_gap_bottom,
@@ -184,7 +189,8 @@ impl Config {
184189

185190
Self {
186191
launch_on_login: true,
187-
gap_size: -2.0,
192+
remove_rounded_corners_on_snap: true,
193+
gap_size: -1.0,
188194
screen_edge_gap_top: 0.0,
189195
screen_edge_gap_bottom: 0.0,
190196
screen_edge_gap_left: 0.0,
@@ -276,6 +282,7 @@ fn config_from_persisted(p: PersistedConfig, default_config: Config) -> Config {
276282

277283
Config {
278284
launch_on_login: p.launch_on_login,
285+
remove_rounded_corners_on_snap: p.remove_rounded_corners_on_snap,
279286
gap_size: p.gap_size,
280287
screen_edge_gap_top: p.screen_edge_gap_top,
281288
screen_edge_gap_bottom: p.screen_edge_gap_bottom,
@@ -320,6 +327,7 @@ pub fn config_from_frontend(p: ConfigForFrontend) -> Config {
320327
};
321328
Config {
322329
launch_on_login: p.launch_on_login,
330+
remove_rounded_corners_on_snap: p.remove_rounded_corners_on_snap,
323331
gap_size: p.gap_size,
324332
screen_edge_gap_top: p.screen_edge_gap_top,
325333
screen_edge_gap_bottom: p.screen_edge_gap_bottom,
@@ -346,6 +354,7 @@ pub fn save(config: &Config) -> Result<(), Box<dyn std::error::Error>> {
346354
let path = config_path();
347355
let p = PersistedConfig {
348356
launch_on_login: config.launch_on_login,
357+
remove_rounded_corners_on_snap: config.remove_rounded_corners_on_snap,
349358
gap_size: config.gap_size,
350359
screen_edge_gap_top: config.screen_edge_gap_top,
351360
screen_edge_gap_bottom: config.screen_edge_gap_bottom,
@@ -391,11 +400,15 @@ mod tests {
391400
let config = Config::default();
392401

393402
assert!(config.launch_on_login);
394-
assert_eq!(config.gap_size, -2.0);
403+
assert!(config.remove_rounded_corners_on_snap);
404+
assert_eq!(config.gap_size, -1.0);
395405
assert_eq!(config.thirds_layout, "Thirds");
396406
assert_eq!(config.hotkeys.len(), 18);
397407
assert_eq!(config.hotkeys[0].action, "LeftHalf");
398-
assert_eq!(config.hotkeys[0].modifiers, MOD_WIN | MOD_ALT | MOD_NOREPEAT);
408+
assert_eq!(
409+
config.hotkeys[0].modifiers,
410+
MOD_WIN | MOD_ALT | MOD_NOREPEAT
411+
);
399412
}
400413

401414
#[test]
@@ -404,6 +417,7 @@ mod tests {
404417
let frontend = config.to_frontend();
405418

406419
assert_eq!(frontend.thirds_layout, "Thirds");
420+
assert!(frontend.remove_rounded_corners_on_snap);
407421
assert_eq!(frontend.hotkeys[0].action, "LeftHalf");
408422
assert_eq!(frontend.hotkeys[0].shortcut, "Win+Alt+Left");
409423
}
@@ -412,6 +426,7 @@ mod tests {
412426
fn config_from_frontend_normalizes_layout_and_restore_action() {
413427
let frontend = ConfigForFrontend {
414428
launch_on_login: false,
429+
remove_rounded_corners_on_snap: false,
415430
gap_size: 0.0,
416431
screen_edge_gap_top: 1.0,
417432
screen_edge_gap_bottom: 2.0,
@@ -435,15 +450,20 @@ mod tests {
435450
assert_eq!(config.hotkeys.len(), 1);
436451
assert_eq!(config.hotkeys[0].action, "Undo");
437452
assert_eq!(config.hotkeys[0].virtual_key, 0x25);
438-
assert_eq!(config.hotkeys[0].modifiers, MOD_WIN | MOD_ALT | MOD_NOREPEAT);
453+
assert_eq!(
454+
config.hotkeys[0].modifiers,
455+
MOD_WIN | MOD_ALT | MOD_NOREPEAT
456+
);
439457
assert!(!config.launch_on_login);
458+
assert!(!config.remove_rounded_corners_on_snap);
440459
assert!(!config.apply_gaps_to_maximize);
441460
}
442461

443462
#[test]
444463
fn config_from_frontend_falls_back_to_defaults_when_hotkeys_are_invalid() {
445464
let frontend = ConfigForFrontend {
446465
launch_on_login: true,
466+
remove_rounded_corners_on_snap: true,
447467
gap_size: 0.0,
448468
screen_edge_gap_top: 0.0,
449469
screen_edge_gap_bottom: 0.0,
@@ -471,6 +491,7 @@ mod tests {
471491
fn config_from_persisted_normalizes_layout_restore_and_shortcuts() {
472492
let persisted = PersistedConfig {
473493
launch_on_login: true,
494+
remove_rounded_corners_on_snap: false,
474495
gap_size: 0.0,
475496
screen_edge_gap_top: 0.0,
476497
screen_edge_gap_bottom: 0.0,
@@ -493,6 +514,7 @@ mod tests {
493514

494515
let config = config_from_persisted(persisted, Config::default());
495516
assert_eq!(config.thirds_layout, "Fourths");
517+
assert!(!config.remove_rounded_corners_on_snap);
496518
assert_eq!(config.hotkeys.len(), 1);
497519
assert_eq!(config.hotkeys[0].action, "Undo");
498520
assert_eq!(config.hotkeys[0].virtual_key, 0x27);
@@ -502,6 +524,7 @@ mod tests {
502524
fn config_from_persisted_supports_legacy_modifier_and_vk_fields() {
503525
let persisted = PersistedConfig {
504526
launch_on_login: true,
527+
remove_rounded_corners_on_snap: true,
505528
gap_size: 0.0,
506529
screen_edge_gap_top: 0.0,
507530
screen_edge_gap_bottom: 0.0,
@@ -525,14 +548,21 @@ mod tests {
525548
let config = config_from_persisted(persisted, Config::default());
526549
assert_eq!(config.thirds_layout, "Thirds");
527550
assert_eq!(config.hotkeys.len(), 1);
528-
assert_eq!(config.hotkeys[0].modifiers, MOD_WIN | MOD_ALT | MOD_NOREPEAT);
529-
assert_eq!(format_shortcut(config.hotkeys[0].modifiers, config.hotkeys[0].virtual_key), "Win+Alt+Left");
551+
assert_eq!(
552+
config.hotkeys[0].modifiers,
553+
MOD_WIN | MOD_ALT | MOD_NOREPEAT
554+
);
555+
assert_eq!(
556+
format_shortcut(config.hotkeys[0].modifiers, config.hotkeys[0].virtual_key),
557+
"Win+Alt+Left"
558+
);
530559
}
531560

532561
#[test]
533562
fn config_from_persisted_falls_back_to_defaults_when_all_hotkeys_are_invalid() {
534563
let persisted = PersistedConfig {
535564
launch_on_login: true,
565+
remove_rounded_corners_on_snap: true,
536566
gap_size: 0.0,
537567
screen_edge_gap_top: 0.0,
538568
screen_edge_gap_bottom: 0.0,

app/src-tauri/src/manager.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,24 @@ use crate::rect::{EngineRect, Rect};
1010
#[cfg(windows)]
1111
use crate::win32::{
1212
enum_monitors, get_cursor_pos, get_foreground_window, get_monitor_from_point,
13-
get_monitor_from_window, get_process_image_name, resolve_snap_target_window, set_cursor_pos,
14-
set_foreground_window, set_window_bounds, try_get_monitor_info, try_get_window_bounds,
13+
get_monitor_from_window, get_process_image_name, get_window_corner_preference,
14+
remove_rounded_corners, resolve_snap_target_window, set_cursor_pos, set_foreground_window,
15+
set_window_bounds, set_window_corner_preference, try_get_monitor_info, try_get_window_bounds,
1516
};
1617
#[cfg(windows)]
1718
use std::collections::HashMap;
1819
#[cfg(windows)]
1920
use std::time::{Duration, Instant};
21+
#[cfg(windows)]
22+
use windows::Win32::Graphics::Dwm::DWM_WINDOW_CORNER_PREFERENCE;
2023

2124
/// Options for Execute (from config).
2225
#[derive(Clone, Debug)]
2326
pub struct ExecuteOptions {
2427
pub use_cursor_screen: bool,
2528
pub move_cursor_after_snap: bool,
2629
pub move_cursor_across_displays: bool,
30+
pub remove_rounded_corners_on_snap: bool,
2731
pub gap_size: f32,
2832
pub update_restore_rect: bool,
2933
pub disabled_process_names: Option<Vec<String>>,
@@ -46,6 +50,7 @@ impl Default for ExecuteOptions {
4650
use_cursor_screen: false,
4751
move_cursor_after_snap: false,
4852
move_cursor_across_displays: false,
53+
remove_rounded_corners_on_snap: true,
4954
gap_size: 0.0,
5055
update_restore_rect: true,
5156
disabled_process_names: None,
@@ -67,6 +72,7 @@ impl Default for ExecuteOptions {
6772
impl From<&Config> for ExecuteOptions {
6873
fn from(c: &Config) -> Self {
6974
Self {
75+
remove_rounded_corners_on_snap: c.remove_rounded_corners_on_snap,
7076
gap_size: c.gap_size,
7177
screen_edge_gap_top: c.screen_edge_gap_top,
7278
screen_edge_gap_bottom: c.screen_edge_gap_bottom,
@@ -231,6 +237,7 @@ fn resolve_section_layout_mode(
231237
pub struct WindowManager {
232238
restore_rects: HashMap<isize, Rect>,
233239
last_actions: HashMap<isize, (WindowAction, Rect)>,
240+
original_corner_preferences: HashMap<isize, DWM_WINDOW_CORNER_PREFERENCE>,
234241
section_cycle_session: Option<SectionCycleSession>,
235242
}
236243

@@ -240,10 +247,28 @@ impl WindowManager {
240247
Self {
241248
restore_rects: HashMap::new(),
242249
last_actions: HashMap::new(),
250+
original_corner_preferences: HashMap::new(),
243251
section_cycle_session: None,
244252
}
245253
}
246254

255+
fn apply_corner_preference(&mut self, hwnd: windows::Win32::Foundation::HWND, key: isize) {
256+
self.original_corner_preferences
257+
.entry(key)
258+
.or_insert_with(|| get_window_corner_preference(hwnd).unwrap_or_default());
259+
let _ = remove_rounded_corners(hwnd);
260+
}
261+
262+
pub fn restore_corner_preference(
263+
&mut self,
264+
hwnd: windows::Win32::Foundation::HWND,
265+
key: isize,
266+
) {
267+
if let Some(preference) = self.original_corner_preferences.remove(&key) {
268+
let _ = set_window_corner_preference(hwnd, preference);
269+
}
270+
}
271+
247272
/// Restore rect for the given window key (for move/size-end hook).
248273
pub fn get_restore_rect(&self, key: isize) -> Option<Rect> {
249274
self.restore_rects.get(&key).copied()
@@ -263,6 +288,7 @@ impl WindowManager {
263288
pub fn clear_window_state(&mut self, key: isize) {
264289
self.restore_rects.remove(&key);
265290
self.last_actions.remove(&key);
291+
self.original_corner_preferences.remove(&key);
266292
if self
267293
.section_cycle_session
268294
.map(|session| session.window_key == key)
@@ -309,6 +335,7 @@ impl WindowManager {
309335
};
310336
let ok = set_window_bounds(hwnd, &restore, false, false);
311337
if ok {
338+
self.restore_corner_preference(hwnd, key);
312339
self.last_actions.remove(&key);
313340
}
314341
return ok;
@@ -344,6 +371,11 @@ impl WindowManager {
344371
if ok {
345372
self.last_actions
346373
.insert(key, (WindowAction::Maximize, dest_rect));
374+
if options.remove_rounded_corners_on_snap {
375+
self.apply_corner_preference(hwnd, key);
376+
} else {
377+
self.restore_corner_preference(hwnd, key);
378+
}
347379
if options.move_cursor_across_displays {
348380
set_cursor_pos(
349381
dest_rect.left + dest_rect.width() / 2,
@@ -421,6 +453,11 @@ impl WindowManager {
421453
if applied {
422454
self.last_actions
423455
.insert(key, (result.resulting_action, target));
456+
if options.remove_rounded_corners_on_snap {
457+
self.apply_corner_preference(hwnd, key);
458+
} else {
459+
self.restore_corner_preference(hwnd, key);
460+
}
424461
if options.move_cursor_after_snap {
425462
set_cursor_pos(
426463
target.left + target.width() / 2,

0 commit comments

Comments
 (0)