1use std::sync::Arc;
10
11use cef::{ImplBrowser, ImplBrowserHost};
12
13use crate::state::AppState;
14
15pub fn get_zoom_factor(state: &Arc<AppState>) -> serde_json::Value {
17 let factor = *state.zoom_factor.lock();
18 serde_json::json!(factor)
19}
20
21pub fn set_zoom_factor(state: &Arc<AppState>, args: &serde_json::Value) -> Result<serde_json::Value, String> {
25 let factor = args
26 .get("factor")
27 .and_then(|v| v.as_f64())
28 .ok_or_else(|| "Missing factor".to_string())?;
29
30 let factor = factor.clamp(0.5, 3.0);
31 *state.zoom_factor.lock() = factor;
32
33 let zoom_level = factor.ln() / 1.2_f64.ln();
37
38 crate::events::emit_event_from_state(state, "zoom-factor-change", &serde_json::json!(factor));
44
45 Ok(serde_json::Value::Null)
46}
47
48pub fn close_window(state: &Arc<AppState>, args: &serde_json::Value) -> Result<serde_json::Value, String> {
52 #[cfg(target_os = "windows")]
53 unsafe {
54 use windows_sys::Win32::UI::WindowsAndMessaging::*;
55 let hwnd = find_own_top_level_window();
56 if !hwnd.is_null() {
57 PostMessageW(hwnd, WM_CLOSE, 0, 0);
58 return Ok(serde_json::Value::Null);
59 }
60 }
61 #[cfg(not(target_os = "windows"))]
62 {
63 let label = args.get("label").and_then(|v| v.as_str()).unwrap_or("main");
64 crate::ui_tasks::post_close_window(state, label);
65 }
66 let _ = (state, args);
67 Ok(serde_json::Value::Null)
68}
69
70pub fn close_window_by_label(
76 state: &Arc<AppState>,
77 args: &serde_json::Value,
78) -> Result<serde_json::Value, String> {
79 let label = args
80 .get("label")
81 .and_then(|v| v.as_str())
82 .ok_or_else(|| "missing label".to_string())?
83 .to_string();
84
85 #[cfg(target_os = "windows")]
86 unsafe {
87 use cef::{ImplBrowser, ImplBrowserHost};
88 use windows_sys::Win32::UI::WindowsAndMessaging::{PostMessageW, WM_CLOSE};
89 if let Some(browser) = state.get_browser(&label) {
91 if let Some(host) = browser.host() {
92 let hwnd = host.window_handle();
93 if !hwnd.0.is_null() {
94 PostMessageW(hwnd.0 as *mut std::ffi::c_void, WM_CLOSE, 0, 0);
95 return Ok(serde_json::Value::Null);
96 }
97 }
98 }
99 return Err(format!("no top-level HWND for label {}", label));
100 }
101
102 #[cfg(not(target_os = "windows"))]
103 {
104 crate::ui_tasks::post_close_window(state, &label);
105 Ok(serde_json::Value::Null)
106 }
107}
108
109pub fn minimize_window(state: &Arc<AppState>, args: &serde_json::Value) -> Result<serde_json::Value, String> {
111 #[cfg(target_os = "windows")]
112 unsafe {
113 use windows_sys::Win32::UI::WindowsAndMessaging::*;
114 let hwnd = find_own_top_level_window();
115 if !hwnd.is_null() {
116 ShowWindow(hwnd, SW_MINIMIZE);
117 return Ok(serde_json::Value::Null);
118 }
119 }
120 #[cfg(not(target_os = "windows"))]
121 {
122 let label = args.get("label").and_then(|v| v.as_str()).unwrap_or("main");
123 crate::ui_tasks::post_minimize_window(state, label);
124 }
125 let _ = (state, args);
126 Ok(serde_json::Value::Null)
127}
128
129pub fn maximize_window(state: &Arc<AppState>, args: &serde_json::Value) -> Result<serde_json::Value, String> {
136 #[cfg(target_os = "windows")]
137 unsafe {
138 use windows_sys::Win32::UI::WindowsAndMessaging::*;
139 let hwnd = find_own_top_level_window();
140 if !hwnd.is_null() {
141 let mut placement: WINDOWPLACEMENT = std::mem::zeroed();
142 placement.length = std::mem::size_of::<WINDOWPLACEMENT>() as u32;
143 GetWindowPlacement(hwnd, &mut placement);
144 if placement.showCmd == SW_MAXIMIZE as u32 {
145 ShowWindow(hwnd, SW_RESTORE);
146 } else {
147 ShowWindow(hwnd, SW_MAXIMIZE);
148 }
149 return Ok(serde_json::Value::Null);
150 }
151 }
152 #[cfg(not(target_os = "windows"))]
153 {
154 let label = args.get("label").and_then(|v| v.as_str()).unwrap_or("main");
155 crate::ui_tasks::post_maximize_window(state, label);
156 }
157 let _ = (state, args);
158 Ok(serde_json::Value::Null)
159}
160
161pub fn get_window_position(state: &Arc<AppState>) -> Result<serde_json::Value, String> {
163 #[cfg(target_os = "windows")]
164 unsafe {
165 use windows_sys::Win32::UI::WindowsAndMessaging::*;
166 use windows_sys::Win32::Foundation::RECT;
167 let hwnd = find_own_top_level_window();
168 if !hwnd.is_null() {
169 let mut rect: RECT = std::mem::zeroed();
170 GetWindowRect(hwnd, &mut rect);
171 return Ok(serde_json::json!({ "x": rect.left, "y": rect.top }));
172 }
173 }
174 let _ = state;
175 Ok(serde_json::json!({ "x": 0, "y": 0 }))
176}
177
178pub fn move_window_by(state: &Arc<AppState>, args: &serde_json::Value) -> Result<serde_json::Value, String> {
187 let dx = args.get("dx").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
188 let dy = args.get("dy").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
189 let label = args.get("label").and_then(|v| v.as_str()).unwrap_or("main");
190
191 #[cfg(target_os = "windows")]
192 unsafe {
193 use windows_sys::Win32::UI::WindowsAndMessaging::*;
194 use windows_sys::Win32::Foundation::RECT;
195 let hwnd = find_own_top_level_window();
196 if !hwnd.is_null() {
197 let mut rect: RECT = std::mem::zeroed();
198 GetWindowRect(hwnd, &mut rect);
199 let width = rect.right - rect.left;
200 let height = rect.bottom - rect.top;
201 SetWindowPos(
202 hwnd,
203 std::ptr::null_mut(),
204 rect.left + dx,
205 rect.top + dy,
206 width,
207 height,
208 0x0014,
210 );
211 return Ok(serde_json::Value::Null);
212 }
213 }
214 #[cfg(not(target_os = "windows"))]
215 crate::ui_tasks::post_move_window(state, label, dx, dy);
216 let _ = (state, label);
217 Ok(serde_json::Value::Null)
218}
219
220pub fn set_window_position(state: &Arc<AppState>, args: &serde_json::Value) -> Result<serde_json::Value, String> {
224 let x = args.get("x").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
225 let y = args.get("y").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
226 let label = args.get("label").and_then(|v| v.as_str()).unwrap_or("main");
227
228 #[cfg(target_os = "windows")]
229 unsafe {
230 use windows_sys::Win32::UI::WindowsAndMessaging::*;
231 use windows_sys::Win32::Foundation::RECT;
232 let hwnd = find_own_top_level_window();
233 if !hwnd.is_null() {
234 let mut rect: RECT = std::mem::zeroed();
235 GetWindowRect(hwnd, &mut rect);
236 let width = rect.right - rect.left;
237 let height = rect.bottom - rect.top;
238 SetWindowPos(
239 hwnd,
240 std::ptr::null_mut(),
241 x,
242 y,
243 width,
244 height,
245 0x0014,
249 );
250 return Ok(serde_json::Value::Null);
251 }
252 }
253 #[cfg(not(target_os = "windows"))]
254 crate::ui_tasks::post_set_window_position(state, label, x, y);
255 let _ = (state, label);
256 Ok(serde_json::Value::Null)
257}
258
259pub fn start_window_drag(state: &Arc<AppState>, args: &serde_json::Value) -> Result<serde_json::Value, String> {
267 #[cfg(target_os = "windows")]
268 unsafe {
269 use windows_sys::Win32::UI::WindowsAndMessaging::*;
270 use windows_sys::Win32::UI::Input::KeyboardAndMouse::ReleaseCapture;
271 let hwnd = find_own_top_level_window();
272 if !hwnd.is_null() {
273 ReleaseCapture();
274 SendMessageW(hwnd, WM_NCLBUTTONDOWN, 2 , 0);
275 return Ok(serde_json::Value::Null);
276 }
277 }
278 #[cfg(not(target_os = "windows"))]
279 {
280 let label = args.get("label").and_then(|v| v.as_str()).unwrap_or("main");
281 crate::ui_tasks::post_start_drag(state, label);
282 }
283 let _ = (state, args);
284 Ok(serde_json::Value::Null)
285}
286
287pub fn set_window_transparency(state: &Arc<AppState>, args: &serde_json::Value) -> Result<serde_json::Value, String> {
294 let transparent = args.get("transparent").and_then(|v| v.as_bool()).unwrap_or(false);
295 let opacity = args.get("opacity").and_then(|v| v.as_f64()).unwrap_or(0.8);
296 let label = args.get("label").and_then(|v| v.as_str()).unwrap_or("main").to_string();
297 tracing::info!("set_window_transparency: label={} transparent={} opacity={}", label, transparent, opacity);
298
299 #[cfg(target_os = "windows")]
300 {
301 let hwnd_raw = state.window_hwnds.lock().get(label.as_str()).copied();
302 let hwnds: Vec<*mut std::ffi::c_void> = if let Some(raw) = hwnd_raw {
303 vec![raw as *mut std::ffi::c_void]
304 } else {
305 tracing::warn!("set_window_transparency: no hwnd for label={}, falling back to find_all_own_windows", label);
309 find_all_own_windows()
310 };
311 for hwnd in hwnds {
312 unsafe {
313 if transparent {
314 apply_window_opacity(hwnd, opacity);
315 } else {
316 remove_window_opacity(hwnd);
317 }
318 }
319 tracing::info!("set_window_transparency: applied to {:?}", hwnd);
320 }
321 }
322
323 let _ = state;
324 #[cfg(not(target_os = "windows"))]
325 let _ = (transparent, opacity);
326
327 Ok(serde_json::Value::Null)
328}
329
330#[cfg(target_os = "windows")]
332fn find_all_own_windows() -> Vec<*mut std::ffi::c_void> {
333 use windows_sys::Win32::UI::WindowsAndMessaging::*;
334 use windows_sys::Win32::System::Threading::GetCurrentProcessId;
335
336 let mut results: Vec<*mut std::ffi::c_void> = Vec::new();
337
338 unsafe extern "system" fn enum_callback(
339 hwnd: *mut std::ffi::c_void,
340 lparam: isize,
341 ) -> i32 {
342 use windows_sys::Win32::System::Threading::GetCurrentProcessId;
343 let mut window_pid: u32 = 0;
344 GetWindowThreadProcessId(hwnd, &mut window_pid);
345 if window_pid == GetCurrentProcessId() && IsWindowVisible(hwnd) != 0 {
346 let results = &mut *(lparam as *mut Vec<*mut std::ffi::c_void>);
347 results.push(hwnd);
348 }
349 1 }
351
352 unsafe {
353 EnumWindows(Some(enum_callback), &mut results as *mut _ as isize);
354 }
355 results
356}
357
358#[cfg(target_os = "windows")]
362pub(crate) unsafe fn find_own_top_level_window() -> *mut std::ffi::c_void {
363 use windows_sys::Win32::UI::WindowsAndMessaging::*;
364 use windows_sys::Win32::System::Threading::GetCurrentProcessId;
365
366 let pid = GetCurrentProcessId();
367 let mut result: *mut std::ffi::c_void = std::ptr::null_mut();
368
369 unsafe extern "system" fn enum_callback(
370 hwnd: *mut std::ffi::c_void,
371 lparam: isize,
372 ) -> i32 {
373 use windows_sys::Win32::System::Threading::GetCurrentProcessId;
374 let mut window_pid: u32 = 0;
375 GetWindowThreadProcessId(hwnd, &mut window_pid);
376 if window_pid == GetCurrentProcessId() && IsWindowVisible(hwnd) != 0 {
377 let result_ptr = lparam as *mut *mut std::ffi::c_void;
379 *result_ptr = hwnd;
380 return 0; }
382 1 }
384
385 let _ = pid; EnumWindows(
387 Some(enum_callback),
388 &mut result as *mut _ as isize,
389 );
390 result
391}
392
393#[cfg(target_os = "windows")]
396unsafe fn apply_window_opacity(hwnd: *mut std::ffi::c_void, opacity: f64) {
397 use windows_sys::Win32::UI::WindowsAndMessaging::*;
398
399 let alpha = (opacity.clamp(0.0, 1.0) * 255.0) as u8;
400
401 let ex_style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
403 SetWindowLongPtrW(hwnd, GWL_EXSTYLE, ex_style | WS_EX_LAYERED as isize);
404
405 let result = SetLayeredWindowAttributes(hwnd, 0, alpha, 0x02);
407 if result != 0 {
408 tracing::info!("Applied window opacity: {} (alpha={})", opacity, alpha);
409 } else {
410 tracing::warn!("SetLayeredWindowAttributes failed");
411 }
412}
413
414#[cfg(target_os = "windows")]
416unsafe fn remove_window_opacity(hwnd: *mut std::ffi::c_void) {
417 use windows_sys::Win32::UI::WindowsAndMessaging::*;
418 let ex_style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
419 if (ex_style & WS_EX_LAYERED as isize) != 0 {
420 SetWindowLongPtrW(hwnd, GWL_EXSTYLE, ex_style & !(WS_EX_LAYERED as isize));
421 tracing::info!("Removed window opacity (WS_EX_LAYERED cleared)");
422 }
423}
424
425pub fn get_window_label(args: &serde_json::Value) -> serde_json::Value {
428 let label = args.get("label").and_then(|v| v.as_str()).unwrap_or("main");
429 serde_json::json!(label)
430}
431
432pub fn is_main_window(args: &serde_json::Value) -> serde_json::Value {
435 let label = args.get("label").and_then(|v| v.as_str()).unwrap_or("main");
436 serde_json::json!(label == "main")
437}
438
439pub fn get_double_click_time() -> serde_json::Value {
450 #[cfg(target_os = "windows")]
451 {
452 let ms = unsafe { windows_sys::Win32::UI::Input::KeyboardAndMouse::GetDoubleClickTime() };
453 serde_json::json!(ms)
454 }
455 #[cfg(not(target_os = "windows"))]
456 {
457 serde_json::json!(500u32)
458 }
459}
460
461pub fn list_window_instances(state: &Arc<AppState>) -> serde_json::Value {
473 let (pool_labels, browsers) = state.user_visibility_snapshot();
478 let labels: Vec<String> = browsers
479 .into_iter()
480 .map(|(l, _)| l)
481 .filter(|l| !pool_labels.contains(l.as_str()) && !l.starts_with("browser-pane-"))
482 .collect();
483 let entries: Vec<serde_json::Value> = labels
488 .iter()
489 .map(|l| {
490 serde_json::json!({
491 "label": l,
492 "windowId": state.backend_window_id(l),
493 })
494 })
495 .collect();
496 serde_json::json!(entries)
497}
498
499pub fn list_windows(state: &Arc<AppState>) -> serde_json::Value {
513 let (pool_labels, browsers) = state.user_visibility_snapshot();
514 let labels: Vec<String> = browsers
515 .into_iter()
516 .map(|(l, _)| l)
517 .filter(|l| !pool_labels.contains(l.as_str()))
518 .collect();
519 serde_json::json!(labels)
520}
521
522pub fn focus_window(state: &Arc<AppState>, args: &serde_json::Value) -> Result<serde_json::Value, String> {
531 let label = args.get("label").and_then(|v| v.as_str()).unwrap_or("main");
532 crate::ui_tasks::post_focus_window(state, label);
533 Ok(serde_json::Value::Null)
534}
535
536pub fn get_instance_number(state: &Arc<AppState>, args: &serde_json::Value) -> serde_json::Value {
542 let label = args
543 .get("label")
544 .and_then(|v| v.as_str())
545 .unwrap_or("main");
546 serde_json::json!(state.instance_num(label).unwrap_or(1))
547}
548
549pub fn register_backend_window(_state: &Arc<AppState>, args: &serde_json::Value) -> serde_json::Value {
553 let label = args.get("label").and_then(|v| v.as_str()).unwrap_or("main");
554 let window_id = args.get("window_id").and_then(|v| v.as_str()).unwrap_or("");
555 tracing::info!(label = %label, window_id = %window_id, "[window] register_backend_window received");
556 crate::client::dlog(&format!("register_backend_window: label={} window_id={}", label, window_id));
557 if !window_id.is_empty() {
558 tracing::info!(label = %label, window_id = %window_id, "[window] registered backend window ID");
564 crate::launcher_ipc::report_backend_window_id_registered(
565 label.to_string(),
566 window_id.to_string(),
567 );
568 } else {
573 tracing::warn!(label = %label, "[window] register_backend_window called with empty window_id — skipped");
574 }
575 serde_json::Value::Null
576}
577
578pub fn toggle_devtools(state: &Arc<AppState>, args: &serde_json::Value) -> Result<serde_json::Value, String> {
585 let label = args.get("label").and_then(|v| v.as_str()).unwrap_or("main");
586 crate::ui_tasks::post_show_dev_tools(state, label);
587 Ok(serde_json::Value::Null)
588}
589
590pub(crate) fn resolve_frontend_base_url(ipc_port: u16) -> String {
594 let mode = agentmux_common::RuntimeMode::from_env().or_else(|| {
602 std::env::current_exe()
603 .ok()
604 .and_then(|p| p.parent().map(|d| d.to_path_buf()))
605 .map(|d| agentmux_common::RuntimeMode::current(&d))
606 });
607 if matches!(mode, Some(agentmux_common::RuntimeMode::Dev { .. })) {
608 return "http://localhost:5173".to_string();
609 }
610 let exe_dir = std::env::current_exe()
611 .ok()
612 .and_then(|p| p.parent().map(|d| d.to_path_buf()));
613 let has_frontend = exe_dir
614 .as_ref()
615 .map(|d| d.join("frontend/index.html").exists())
616 .unwrap_or(false);
617 if has_frontend {
618 format!("http://127.0.0.1:{}", ipc_port)
619 } else {
620 "http://localhost:5173".to_string()
621 }
622}
623
624pub fn open_new_window(state: &Arc<AppState>) -> Result<serde_json::Value, String> {
629 open_window_with_kind(state, crate::state::WindowKind::FullInstance, None)
630}
631
632pub fn open_subwindow(
638 state: &Arc<AppState>,
639 parent_instance_id: String,
640) -> Result<serde_json::Value, String> {
641 let parent_ok = state
660 .window_meta
661 .lock()
662 .get(&parent_instance_id)
663 .map(|m| m.kind == crate::state::WindowKind::FullInstance)
664 .unwrap_or(false);
665 if !parent_ok {
666 return Err(format!(
667 "open_subwindow: unknown or non-full-instance parent label={parent_instance_id}"
668 ));
669 }
670 open_window_with_kind(
671 state,
672 crate::state::WindowKind::Subwindow,
673 Some(parent_instance_id),
674 )
675}
676
677fn open_window_with_kind(
678 state: &Arc<AppState>,
679 kind: crate::state::WindowKind,
680 parent_instance_id: Option<String>,
681) -> Result<serde_json::Value, String> {
682 if state.any_browser_pane_closing() {
689 tracing::warn!(
690 target: "wfr:gate",
691 "[wfr:gate] open_window refused — pane is mid-close (H.7 invariant)"
692 );
693 return Err("a pane is currently closing; retry shortly".to_string());
694 }
695
696 let window_id = uuid::Uuid::new_v4();
697 let label = format!("window-{}", window_id.simple());
698
699 let ipc_port = *state.ipc_port.lock();
700 let ipc_token = &state.ipc_token;
701 let base_url = resolve_frontend_base_url(ipc_port);
702
703 let separator = if base_url.contains('?') { "&" } else { "?" };
704 let url = format!(
705 "{}{}ipc_port={}&ipc_token={}&windowLabel={}",
706 base_url, separator, ipc_port, ipc_token, label
707 );
708
709 tracing::info!(label = %label, kind = ?kind, parent = ?parent_instance_id, "[window] open window");
710
711 let (pos_x, pos_y) = get_offset_position();
715 let (win_w, win_h) = get_secondary_window_size(pos_x, pos_y);
716
717 state.host_dispatch(
719 crate::reducer::HostCommand::EnqueuePendingWindowCreation {
720 entry: crate::state::PendingWindowCreation {
721 label: label.clone(),
722 kind,
723 parent_instance_id,
724 },
725 },
726 );
727
728 crate::ui_tasks::post_create_window(
731 state, &url, &label, pos_x, pos_y, win_w, win_h,
732 true,
733 );
734
735 Ok(serde_json::json!(label))
739}
740
741fn get_offset_position() -> (i32, i32) {
743 #[cfg(target_os = "windows")]
744 unsafe {
745 use windows_sys::Win32::UI::WindowsAndMessaging::*;
746 use windows_sys::Win32::Foundation::RECT;
747 let hwnd = find_own_top_level_window();
748 if !hwnd.is_null() {
749 let mut rect: RECT = std::mem::zeroed();
750 GetWindowRect(hwnd, &mut rect);
751 return (rect.left + 30, rect.top + 30);
752 }
753 }
754 #[cfg(target_os = "windows")]
755 {
756 use windows_sys::Win32::UI::WindowsAndMessaging::CW_USEDEFAULT;
757 return (CW_USEDEFAULT, CW_USEDEFAULT);
758 }
759 #[cfg(not(target_os = "windows"))]
760 (100, 100)
761}
762
763fn get_secondary_window_size(px: i32, py: i32) -> (i32, i32) {
766 #[cfg(target_os = "windows")]
767 {
768 use crate::app::get_monitor_work_area;
769 if let Some((_x, _y, work_w, work_h)) = get_monitor_work_area(px, py) {
770 return ((work_w as f64 * 0.70) as i32, (work_h as f64 * 0.70) as i32);
771 }
772 }
773 (1200, 800)
774}
775
776#[cfg(target_os = "windows")]
788pub(crate) fn capture_hwnd_for_label(state: &Arc<AppState>, label: &str) {
789 use cef::ImplBrowserHost;
790 if let Some(mut browser) = state.get_browser(label) {
792 if let Some(host) = browser.host() {
793 let hwnd = host.window_handle();
794 if !hwnd.0.is_null() {
795 state.window_hwnds.lock().insert(label.to_string(), hwnd.0 as isize);
796 tracing::debug!("[opacity] captured hwnd fast-path label={} hwnd={:#x}", label, hwnd.0 as isize);
797 return;
798 }
799 }
800 }
801 let known: std::collections::HashSet<isize> = state.window_hwnds.lock().values().cloned().collect();
803 for hwnd_raw in find_all_own_windows() {
804 let raw = hwnd_raw as isize;
805 if !known.contains(&raw) {
806 state.window_hwnds.lock().insert(label.to_string(), raw);
807 tracing::debug!("[opacity] captured hwnd fallback label={} hwnd={:#x}", label, raw);
808 return;
809 }
810 }
811 tracing::warn!("[opacity] capture_hwnd_for_label: no available HWND for label={}", label);
812}
813
814pub fn set_window_opacity(
822 state: &Arc<AppState>,
823 args: &serde_json::Value,
824) -> Result<serde_json::Value, String> {
825 let label = args
826 .get("label")
827 .and_then(|v| v.as_str())
828 .ok_or("set_window_opacity: missing label")?
829 .to_string();
830 let opacity = args
831 .get("opacity")
832 .and_then(|v| v.as_f64())
833 .unwrap_or(1.0)
834 .clamp(0.0, 1.0);
835
836 let out = state.host_dispatch(crate::reducer::HostCommand::SetWindowOpacity {
837 label: label.clone(),
838 opacity: opacity as f32,
839 });
840
841 #[cfg(target_os = "windows")]
848 for ev in &out.events {
849 match ev {
850 crate::reducer::HostEvent::WindowOpacityApplied { label: ev_label, opacity: ev_opacity, .. } => {
851 let hwnd_raw = state.window_hwnds.lock().get(ev_label.as_str()).copied();
852 if let Some(raw) = hwnd_raw {
853 let hwnd = raw as *mut std::ffi::c_void;
854 unsafe { apply_window_opacity(hwnd, *ev_opacity as f64); }
855 } else {
856 tracing::warn!("[opacity] set_window_opacity: no hwnd for label={}", ev_label);
857 }
858 }
859 crate::reducer::HostEvent::WindowOpacityCleared { label: ev_label, .. } => {
860 let hwnd_raw = state.window_hwnds.lock().get(ev_label.as_str()).copied();
861 if let Some(raw) = hwnd_raw {
862 let hwnd = raw as *mut std::ffi::c_void;
863 unsafe { remove_window_opacity(hwnd); }
864 } else {
865 tracing::warn!("[opacity] set_window_opacity: no hwnd for label={} (clear)", ev_label);
866 }
867 }
868 _ => {}
869 }
870 }
871
872 let _ = out;
873 Ok(serde_json::Value::Null)
874}
875
876pub fn get_window_opacity(
882 state: &Arc<AppState>,
883 args: &serde_json::Value,
884) -> Result<serde_json::Value, String> {
885 let label = args
886 .get("label")
887 .and_then(|v| v.as_str())
888 .unwrap_or("main");
889 let opacity = state
890 .host_state
891 .lock()
892 .window_opacities
893 .get(label)
894 .copied()
895 .unwrap_or(1.0);
896 Ok(serde_json::json!(opacity))
897}