feat(windows): add a normal window mode switch
Add a 普通窗口模式 switch to the general settings section. When enabled the window is created with WS_EX_APPWINDOW instead of WS_EX_TOOLWINDOW, so it appears in the taskbar and Alt+Tab, and losing focus no longer hides it. The extended style switches live without a restart; a failed save rolls the checkbox back and reports the error, and the general section now reports save failures instead of discarding them. The switch is drawn anti-aliased. GDI's RoundRect has no smoothing, and at 22 logical pixels tall the staircase on the pill and the knob read as a low-resolution control, so both shapes are rasterized together at 4x and halftone-downscaled. The off track uses p.faint: p.line is the card border and p.pill is the card fill, so the previous colours made the switch disappear into the card it sits on.
This commit is contained in:
1 parent
6b59eb12e5
commit
1fde401b3a
4 files changed
+300
-26
No files matched your search
+122
-8
@@ -65,7 +65,9 @@ const WS_VSCROLL: u32 = 0x00200000;
|
||||
const WS_BORDER: u32 = 0x00800000;
|
||||
|
||||
const WS_EX_TOOLWINDOW: u32 = 0x00000080;
|
||||
const WS_EX_APPWINDOW: u32 = 0x00040000;
|
||||
const WS_EX_LAYERED: u32 = 0x00080000;
|
||||
const GWL_EXSTYLE: i32 = -20;
|
||||
|
||||
const ES_MULTILINE: u32 = 0x0004;
|
||||
const ES_AUTOVSCROLL: u32 = 0x0040;
|
||||
@@ -125,6 +127,8 @@ const HWND_NOTOPMOST: HWND = -2isize as HWND;
|
||||
const SWP_NOMOVE: u32 = 0x0002;
|
||||
const SWP_NOSIZE: u32 = 0x0001;
|
||||
const SWP_NOACTIVATE: u32 = 0x0010;
|
||||
const SWP_NOZORDER: u32 = 0x0004;
|
||||
const SWP_FRAMECHANGED: u32 = 0x0020;
|
||||
|
||||
const LWA_ALPHA: u32 = 0x00000002;
|
||||
const TRANSPARENT: i32 = 1;
|
||||
@@ -215,6 +219,7 @@ const ID_BTN_UPDATE: usize = 428;
|
||||
const ID_CHK_AUTOSTART: usize = 425;
|
||||
const ID_BTN_SAVE_SETTINGS: usize = 426;
|
||||
const ID_BTN_HYMT_DELETE: usize = 427;
|
||||
const ID_CHK_NORMAL_WINDOW: usize = 429;
|
||||
|
||||
const ID_TRAY_TRANSLATE: usize = 901;
|
||||
const ID_TRAY_OCR: usize = 902;
|
||||
@@ -653,6 +658,7 @@ struct Win32AppState {
|
||||
cmb_opacity: HWND,
|
||||
chk_ocr_newline: HWND,
|
||||
chk_autostart: HWND,
|
||||
chk_normal_window: HWND,
|
||||
btn_save_settings: HWND,
|
||||
lbl_settings_status: HWND,
|
||||
}
|
||||
@@ -740,7 +746,13 @@ pub fn run() {
|
||||
|
||||
let title = wide_null("TTREE");
|
||||
let hwnd = CreateWindowExW(
|
||||
WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_COMPOSITED,
|
||||
WS_EX_LAYERED
|
||||
| WS_EX_COMPOSITED
|
||||
| if settings.appearance().normal_window_mode {
|
||||
WS_EX_APPWINDOW
|
||||
} else {
|
||||
WS_EX_TOOLWINDOW
|
||||
},
|
||||
class_name.as_ptr(),
|
||||
title.as_ptr(),
|
||||
WS_OVERLAPPED | WS_THICKFRAME | WS_CLIPCHILDREN,
|
||||
@@ -917,6 +929,7 @@ pub fn run() {
|
||||
cmb_opacity: ptr::null_mut(),
|
||||
chk_ocr_newline: ptr::null_mut(),
|
||||
chk_autostart: ptr::null_mut(),
|
||||
chk_normal_window: ptr::null_mut(),
|
||||
updater: Default::default(),
|
||||
update_view: Default::default(),
|
||||
btn_update: ptr::null_mut(),
|
||||
@@ -1529,6 +1542,14 @@ impl Win32AppState {
|
||||
ID_CHK_AUTOSTART,
|
||||
false,
|
||||
);
|
||||
self.chk_normal_window = self.create_ctrl(
|
||||
hinst,
|
||||
"BUTTON",
|
||||
"普通窗口模式",
|
||||
BS_AUTOCHECKBOX,
|
||||
ID_CHK_NORMAL_WINDOW,
|
||||
false,
|
||||
);
|
||||
self.btn_update = self.create_ctrl(
|
||||
hinst,
|
||||
"BUTTON",
|
||||
@@ -1692,6 +1713,7 @@ impl Win32AppState {
|
||||
self.cmb_opacity,
|
||||
self.chk_ocr_newline,
|
||||
self.chk_autostart,
|
||||
self.chk_normal_window,
|
||||
self.btn_update,
|
||||
self.lbl_update,
|
||||
]
|
||||
@@ -2050,6 +2072,7 @@ impl Win32AppState {
|
||||
self.cmb_opacity,
|
||||
self.chk_ocr_newline,
|
||||
self.chk_autostart,
|
||||
self.chk_normal_window,
|
||||
self.btn_update,
|
||||
self.lbl_update,
|
||||
],
|
||||
@@ -2082,9 +2105,9 @@ impl Win32AppState {
|
||||
ID_SETTINGS_KEYS => 82 + row_h * 5 + 32 + 90,
|
||||
_ => {
|
||||
if wide {
|
||||
490
|
||||
568
|
||||
} else {
|
||||
530
|
||||
608
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2314,9 +2337,13 @@ impl Win32AppState {
|
||||
}
|
||||
_ => {
|
||||
let opacity_h = if wide { 84 } else { 112 };
|
||||
bg.card(BoxRect::new(x, top, fw, opacity_h + 286), p);
|
||||
bg.card(BoxRect::new(x, top, fw, opacity_h + 364), p);
|
||||
form_row(self.lbl_opacity, self.cmb_opacity, top + 18, 32, &mut bg);
|
||||
for (i, (c, description)) in [
|
||||
(
|
||||
self.chk_normal_window,
|
||||
"显示在任务栏和 Alt+Tab 中,切走时不收起",
|
||||
),
|
||||
(self.chk_ocr_newline, "合并识别文本中的段落换行"),
|
||||
(self.chk_autostart, "登录 Windows 时自动启动"),
|
||||
]
|
||||
@@ -2328,13 +2355,18 @@ impl Win32AppState {
|
||||
place(c, x + inset, y + 15, fw - inset * 2, 26);
|
||||
show(c);
|
||||
bg.label(
|
||||
BoxRect::new(x + inset + 25, y + 44, fw - inset * 2 - 25, 18),
|
||||
BoxRect::new(
|
||||
x + inset + if c == self.chk_normal_window { 0 } else { 25 },
|
||||
y + 44,
|
||||
fw - inset * 2 - if c == self.chk_normal_window { 0 } else { 25 },
|
||||
18,
|
||||
),
|
||||
description,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
let update_y = top + opacity_h + 168;
|
||||
let update_y = top + opacity_h + 246;
|
||||
place(self.btn_update, x + inset, update_y, 140, 32);
|
||||
place(
|
||||
self.lbl_update,
|
||||
@@ -2419,6 +2451,66 @@ impl Win32AppState {
|
||||
InvalidateRect(self.btn_pin, ptr::null(), 0);
|
||||
}
|
||||
|
||||
unsafe fn apply_normal_window_mode(&self, enabled: bool) {
|
||||
let current = GetWindowLongPtrW(self.hwnd, GWL_EXSTYLE) as u32;
|
||||
let style = (current & !(WS_EX_APPWINDOW | WS_EX_TOOLWINDOW))
|
||||
| if enabled {
|
||||
WS_EX_APPWINDOW
|
||||
} else {
|
||||
WS_EX_TOOLWINDOW
|
||||
};
|
||||
if current == style {
|
||||
return;
|
||||
}
|
||||
let visible = IsWindowVisible(self.hwnd) != 0;
|
||||
let focus = GetFocus();
|
||||
// The shell updates taskbar membership when a window is hidden and shown.
|
||||
// Do not use hide_window here: switching modes must keep active requests.
|
||||
if visible {
|
||||
ShowWindow(self.hwnd, SW_HIDE);
|
||||
}
|
||||
SetWindowLongPtrW(self.hwnd, GWL_EXSTYLE, style as isize);
|
||||
SetWindowPos(
|
||||
self.hwnd,
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED,
|
||||
);
|
||||
if visible {
|
||||
ShowWindow(self.hwnd, SW_SHOW);
|
||||
SetForegroundWindow(self.hwnd);
|
||||
if !focus.is_null() {
|
||||
SetFocus(focus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn save_normal_window_mode(&self) {
|
||||
let mut cfg = self.settings.appearance();
|
||||
let previous = cfg.normal_window_mode;
|
||||
cfg.normal_window_mode =
|
||||
SendMessageW(self.chk_normal_window, BM_GETCHECK, 0, 0) as usize == BST_CHECKED;
|
||||
if let Err(error) = self.settings.save_appearance(cfg) {
|
||||
SendMessageW(
|
||||
self.chk_normal_window,
|
||||
BM_SETCHECK,
|
||||
if previous { BST_CHECKED } else { BST_UNCHECKED },
|
||||
0,
|
||||
);
|
||||
set_hwnd_text(
|
||||
self.lbl_settings_status,
|
||||
&format!("保存窗口模式失败: {error}"),
|
||||
);
|
||||
} else {
|
||||
self.apply_normal_window_mode(cfg.normal_window_mode);
|
||||
set_hwnd_text(self.lbl_settings_status, "窗口模式已保存");
|
||||
}
|
||||
InvalidateRect(self.chk_normal_window, ptr::null(), 0);
|
||||
}
|
||||
|
||||
unsafe fn show_and_focus(&mut self) {
|
||||
ShowWindow(self.hwnd, SW_RESTORE);
|
||||
ShowWindow(self.hwnd, SW_SHOW);
|
||||
@@ -2969,6 +3061,16 @@ impl Win32AppState {
|
||||
};
|
||||
SendMessageW(self.cmb_opacity, CB_SETCURSEL, op_idx, 0);
|
||||
|
||||
SendMessageW(
|
||||
self.chk_normal_window,
|
||||
BM_SETCHECK,
|
||||
if app_cfg.normal_window_mode {
|
||||
BST_CHECKED
|
||||
} else {
|
||||
BST_UNCHECKED
|
||||
},
|
||||
0,
|
||||
);
|
||||
SendMessageW(
|
||||
self.chk_ocr_newline,
|
||||
BM_SETCHECK,
|
||||
@@ -3075,8 +3177,15 @@ impl Win32AppState {
|
||||
let app_cfg = AppearanceConfig {
|
||||
panel_opacity: opacity,
|
||||
ocr_strip_newlines: ocr_strip,
|
||||
normal_window_mode: self.settings.appearance().normal_window_mode,
|
||||
};
|
||||
let _ = self.settings.save_appearance(app_cfg);
|
||||
if let Err(error) = self.settings.save_appearance(app_cfg) {
|
||||
set_hwnd_text(
|
||||
self.lbl_settings_status,
|
||||
&format!("保存通用设置失败: {error}"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let alpha = (opacity * 255.0).round() as u8;
|
||||
SetLayeredWindowAttributes(self.hwnd, 0, alpha, LWA_ALPHA);
|
||||
|
||||
@@ -3450,7 +3559,11 @@ unsafe extern "system" fn main_wnd_proc(
|
||||
if active_state == WA_INACTIVE {
|
||||
with_state(|st| {
|
||||
st.set_shortcut_recording(false);
|
||||
if !st.pinned && !st.ocr_in_flight && st.active_tab != TabMode::Settings {
|
||||
if !st.pinned
|
||||
&& !st.settings.appearance().normal_window_mode
|
||||
&& !st.ocr_in_flight
|
||||
&& st.active_tab != TabMode::Settings
|
||||
{
|
||||
st.hide_window();
|
||||
}
|
||||
});
|
||||
@@ -3608,6 +3721,7 @@ unsafe fn handle_wm_command(st: &mut Win32AppState, id: usize, code: u16) {
|
||||
}
|
||||
ID_BTN_OCR if code == BN_CLICKED => st.trigger_ocr(),
|
||||
ID_BTN_PIN if code == BN_CLICKED => st.set_pinned(!st.pinned),
|
||||
ID_CHK_NORMAL_WINDOW if code == BN_CLICKED => st.save_normal_window_mode(),
|
||||
|
||||
ID_EDIT_SOURCE if code == EN_CHANGE => {
|
||||
st.refresh_source_input();
|
||||
|
||||
+140
-18
@@ -50,6 +50,7 @@ const DT_CALCRECT: u32 = 0x400;
|
||||
const DT_NOPREFIX: u32 = 0x800;
|
||||
const DT_END_ELLIPSIS: u32 = 0x8000;
|
||||
const SRCCOPY: u32 = 0x00cc0020;
|
||||
const HALFTONE: i32 = 4;
|
||||
|
||||
#[repr(C)]
|
||||
struct PAINTSTRUCT {
|
||||
@@ -110,7 +111,8 @@ extern "system" {
|
||||
pub fn GetDpiForSystem() -> u32;
|
||||
pub fn GetWindowRect(hwnd: HWND, rect: *mut RECT) -> BOOL;
|
||||
fn GetDlgCtrlID(hwnd: HWND) -> i32;
|
||||
fn GetWindowLongPtrW(hwnd: HWND, index: i32) -> isize;
|
||||
pub fn GetWindowLongPtrW(hwnd: HWND, index: i32) -> isize;
|
||||
pub fn SetWindowLongPtrW(hwnd: HWND, index: i32, value: isize) -> isize;
|
||||
fn SetViewportOrgEx(hdc: HDC, x: i32, y: i32, old: *mut POINT) -> BOOL;
|
||||
fn RedrawWindow(hwnd: HWND, rect: *const RECT, region: HANDLE, flags: u32) -> BOOL;
|
||||
pub fn GetFocus() -> HWND;
|
||||
@@ -158,6 +160,20 @@ extern "system" {
|
||||
sy: i32,
|
||||
rop: u32,
|
||||
) -> BOOL;
|
||||
fn StretchBlt(
|
||||
dest: HDC,
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: i32,
|
||||
h: i32,
|
||||
src: HDC,
|
||||
sx: i32,
|
||||
sy: i32,
|
||||
sw: i32,
|
||||
sh: i32,
|
||||
rop: u32,
|
||||
) -> BOOL;
|
||||
fn SetStretchBltMode(hdc: HDC, mode: i32) -> i32;
|
||||
}
|
||||
#[link(name = "comctl32")]
|
||||
extern "system" {
|
||||
@@ -797,28 +813,95 @@ fn px(r: BoxRect) -> RECT {
|
||||
}
|
||||
}
|
||||
unsafe fn fill(hdc: HDC, r: RECT, color: u32, radius: i32) {
|
||||
let brush = CreateSolidBrush(color);
|
||||
if radius == 0 {
|
||||
let brush = CreateSolidBrush(color);
|
||||
FillRect(hdc, &r, brush);
|
||||
DeleteObject(brush);
|
||||
} else {
|
||||
let pen = CreatePen(0, 1, color);
|
||||
let old_b = SelectObject(hdc, brush);
|
||||
let old_p = SelectObject(hdc, pen);
|
||||
RoundRect(
|
||||
hdc,
|
||||
r.left,
|
||||
r.top,
|
||||
r.right,
|
||||
r.bottom,
|
||||
scale(radius * 2),
|
||||
scale(radius * 2),
|
||||
);
|
||||
SelectObject(hdc, old_b);
|
||||
SelectObject(hdc, old_p);
|
||||
DeleteObject(pen);
|
||||
rounded(hdc, r, color, scale(radius * 2));
|
||||
}
|
||||
}
|
||||
/// Solid rounded rectangle at the caller's scale. The pen matches the brush so
|
||||
/// the outline does not introduce a second color. GDI rounds corners without
|
||||
/// anti-aliasing, so pass a supersampled rect for smooth edges.
|
||||
unsafe fn rounded(hdc: HDC, r: RECT, color: u32, diameter: i32) {
|
||||
let brush = CreateSolidBrush(color);
|
||||
let pen = CreatePen(0, 1, color);
|
||||
let old_b = SelectObject(hdc, brush);
|
||||
let old_p = SelectObject(hdc, pen);
|
||||
RoundRect(hdc, r.left, r.top, r.right, r.bottom, diameter, diameter);
|
||||
SelectObject(hdc, old_b);
|
||||
SelectObject(hdc, old_p);
|
||||
DeleteObject(pen);
|
||||
DeleteObject(brush);
|
||||
}
|
||||
/// Draws the settings switch. GDI's `RoundRect` is not anti-aliased, and at 22
|
||||
/// logical pixels tall the staircase on the pill and on the knob reads as a
|
||||
/// low-resolution control. Both shapes are rasterized together at 4x and
|
||||
/// halftone downscaled. They share one offscreen pass on purpose: downscaling
|
||||
/// them separately would blend the knob's edge with the card instead of the
|
||||
/// track and leave a halo.
|
||||
unsafe fn draw_switch(
|
||||
hdc: HDC,
|
||||
track: RECT,
|
||||
thumb: RECT,
|
||||
track_color: u32,
|
||||
thumb_color: u32,
|
||||
background: u32,
|
||||
) {
|
||||
const FACTOR: i32 = 4;
|
||||
let width = (track.right - track.left).max(1);
|
||||
let height = (track.bottom - track.top).max(1);
|
||||
let dc = CreateCompatibleDC(hdc);
|
||||
let bitmap = CreateCompatibleBitmap(hdc, width * FACTOR, height * FACTOR);
|
||||
if dc.is_null() || bitmap.is_null() {
|
||||
if !dc.is_null() {
|
||||
DeleteDC(dc);
|
||||
}
|
||||
if !bitmap.is_null() {
|
||||
DeleteObject(bitmap);
|
||||
}
|
||||
fill(hdc, track, track_color, height / 2);
|
||||
fill(hdc, thumb, thumb_color, (thumb.bottom - thumb.top) / 2);
|
||||
return;
|
||||
}
|
||||
let old_bitmap = SelectObject(dc, bitmap);
|
||||
let area = RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: width * FACTOR,
|
||||
bottom: height * FACTOR,
|
||||
};
|
||||
let backdrop = CreateSolidBrush(background);
|
||||
FillRect(dc, &area, backdrop);
|
||||
DeleteObject(backdrop);
|
||||
rounded(dc, area, track_color, height * FACTOR);
|
||||
let knob = RECT {
|
||||
left: (thumb.left - track.left) * FACTOR,
|
||||
top: (thumb.top - track.top) * FACTOR,
|
||||
right: (thumb.right - track.left) * FACTOR,
|
||||
bottom: (thumb.bottom - track.top) * FACTOR,
|
||||
};
|
||||
rounded(dc, knob, thumb_color, (thumb.bottom - thumb.top) * FACTOR);
|
||||
let mode = SetStretchBltMode(hdc, HALFTONE);
|
||||
StretchBlt(
|
||||
hdc,
|
||||
track.left,
|
||||
track.top,
|
||||
width,
|
||||
height,
|
||||
dc,
|
||||
0,
|
||||
0,
|
||||
width * FACTOR,
|
||||
height * FACTOR,
|
||||
SRCCOPY,
|
||||
);
|
||||
SetStretchBltMode(hdc, mode);
|
||||
SelectObject(dc, old_bitmap);
|
||||
DeleteObject(bitmap);
|
||||
DeleteDC(dc);
|
||||
}
|
||||
unsafe fn text(hdc: HDC, s: &str, r: RECT, font: HFONT, color: u32, flags: u32) {
|
||||
let old = SelectObject(hdc, font);
|
||||
SetBkMode(hdc, TRANSPARENT);
|
||||
@@ -1291,6 +1374,45 @@ unsafe fn draw_checkbox(hwnd: HWND, hdc: HDC) {
|
||||
fill(hdc, r, background, 0);
|
||||
let center = (r.top + r.bottom) / 2;
|
||||
let check = SendMessageW(hwnd, BM_GETCHECK, 0, 0) as usize == BST_CHECKED;
|
||||
if GetDlgCtrlID(hwnd) as usize == ID_CHK_NORMAL_WINDOW {
|
||||
let track = RECT {
|
||||
left: r.right - scale(40),
|
||||
top: center - scale(11),
|
||||
right: r.right,
|
||||
bottom: center + scale(11),
|
||||
};
|
||||
let thumb_left = track.left + scale(if check { 20 } else { 2 });
|
||||
let thumb = RECT {
|
||||
left: thumb_left,
|
||||
top: track.top + scale(2),
|
||||
right: thumb_left + scale(18),
|
||||
bottom: track.bottom - scale(2),
|
||||
};
|
||||
// `p.line` is the card border and `p.pill` is the card fill, so an off
|
||||
// track drawn with them disappears into the card it sits on.
|
||||
draw_switch(
|
||||
hdc,
|
||||
track,
|
||||
thumb,
|
||||
if check { p.ink } else { p.faint },
|
||||
p.pill,
|
||||
background,
|
||||
);
|
||||
r.right = track.left - scale(12);
|
||||
let f = VISUALS.with(|v| v.borrow().regular);
|
||||
text(
|
||||
hdc,
|
||||
&get_hwnd_text(hwnd),
|
||||
r,
|
||||
f,
|
||||
p.ink,
|
||||
DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS,
|
||||
);
|
||||
if GetFocus() == hwnd {
|
||||
DrawFocusRect(hdc, &r);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let box_r = RECT {
|
||||
left: scale(1),
|
||||
top: center - scale(7),
|
||||
@@ -1381,7 +1503,7 @@ unsafe extern "system" fn control_proc(
|
||||
}
|
||||
let checkbox = matches!(
|
||||
GetDlgCtrlID(hwnd) as usize,
|
||||
ID_CHK_AI_ENABLED | ID_CHK_OCR_NEWLINE | ID_CHK_AUTOSTART
|
||||
ID_CHK_AI_ENABLED | ID_CHK_OCR_NEWLINE | ID_CHK_AUTOSTART | ID_CHK_NORMAL_WINDOW
|
||||
);
|
||||
let interactive =
|
||||
checkbox || combo != 0 || GetWindowLongPtrW(hwnd, -16) as u32 & 0xf == BS_OWNERDRAW;
|
||||
|
||||
@@ -358,6 +358,9 @@ pub struct AppearanceConfig {
|
||||
pub panel_opacity: f64,
|
||||
#[serde(default = "default_true")]
|
||||
pub ocr_strip_newlines: bool,
|
||||
/// Windows 普通窗口:显示在任务栏与 Alt+Tab 中,失焦时保持显示。
|
||||
#[serde(default)]
|
||||
pub normal_window_mode: bool,
|
||||
}
|
||||
|
||||
fn default_panel_opacity() -> f64 {
|
||||
@@ -369,6 +372,7 @@ impl Default for AppearanceConfig {
|
||||
Self {
|
||||
panel_opacity: default_panel_opacity(),
|
||||
ocr_strip_newlines: true,
|
||||
normal_window_mode: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +297,7 @@ impl SettingsStore {
|
||||
let clamped = AppearanceConfig {
|
||||
panel_opacity: AppearanceConfig::clamp_opacity(cfg.panel_opacity),
|
||||
ocr_strip_newlines: cfg.ocr_strip_newlines,
|
||||
normal_window_mode: cfg.normal_window_mode,
|
||||
};
|
||||
let mut guard = self.cache.write().expect("settings lock poisoned");
|
||||
guard.raw_map.insert(
|
||||
@@ -307,6 +308,10 @@ impl SettingsStore {
|
||||
"appearance.ocr_strip_newlines".into(),
|
||||
Value::from(clamped.ocr_strip_newlines),
|
||||
);
|
||||
guard.raw_map.insert(
|
||||
"appearance.normal_window_mode".into(),
|
||||
Value::from(clamped.normal_window_mode),
|
||||
);
|
||||
write_json_map(&self.path, &guard.raw_map)?;
|
||||
guard.appearance = clamped;
|
||||
Ok(())
|
||||
@@ -550,6 +555,12 @@ fn load_appearance(map: &Map<String, Value>) -> AppearanceConfig {
|
||||
{
|
||||
cfg.ocr_strip_newlines = v;
|
||||
}
|
||||
if let Some(v) = map
|
||||
.get("appearance.normal_window_mode")
|
||||
.and_then(Value::as_bool)
|
||||
{
|
||||
cfg.normal_window_mode = v;
|
||||
}
|
||||
cfg
|
||||
}
|
||||
|
||||
@@ -589,6 +600,29 @@ fn load_window_size(map: &Map<String, Value>) -> Option<(f64, f64)> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn normal_window_mode_defaults_off_for_existing_settings_and_survives_reload() {
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
fs::write(
|
||||
tmp.path(),
|
||||
r#"{"appearance.panel_opacity":0.8,"appearance.ocr_strip_newlines":false}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let store = SettingsStore::open_at(tmp.path().to_path_buf());
|
||||
assert!(!store.appearance().normal_window_mode);
|
||||
|
||||
for enabled in [true, false] {
|
||||
let mut appearance = store.appearance();
|
||||
appearance.normal_window_mode = enabled;
|
||||
store.save_appearance(appearance).unwrap();
|
||||
|
||||
let reloaded = SettingsStore::open_at(tmp.path().to_path_buf());
|
||||
assert_eq!(reloaded.appearance().normal_window_mode, enabled);
|
||||
assert_eq!(reloaded.appearance().panel_opacity, 0.8);
|
||||
assert!(!reloaded.appearance().ocr_strip_newlines);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persists_and_reloads_settings_and_api_key_in_json() {
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
|
||||
Reference in new issue
Block a user