Skip to content

Commit c0beab4

Browse files
committed
feat: click through
1 parent e523d9b commit c0beab4

8 files changed

Lines changed: 173 additions & 7 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ resolver = "2"
33
members = ["crates/*"]
44

55
[workspace.package]
6-
version = "3.0.3"
6+
version = "3.0.4"
77
edition = "2021"
88
authors = ["JLiverTool Contributors"]
99
license = "MIT"

crates/jlivertool-ui/src/app.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ pub fn run_app_with_tray(
262262
let pending_open_settings = Arc::new(std::sync::atomic::AtomicBool::new(false));
263263
let pending_open_settings_for_spawn = pending_open_settings.clone();
264264

265+
// Create shared flag for click-through toggle from tray
266+
let pending_click_through = Arc::new(std::sync::atomic::AtomicBool::new(false));
267+
let pending_click_through_for_spawn = pending_click_through.clone();
268+
265269
// Create main window bounds - use saved or default
266270
let bounds = if main_window_config.width > 0 && main_window_config.height > 0 {
267271
Bounds::new(
@@ -320,6 +324,8 @@ pub fn run_app_with_tray(
320324
}
321325
// Set shared pending_open_settings flag
322326
view.set_pending_open_settings_flag(pending_open_settings.clone());
327+
// Set shared pending_click_through flag
328+
view.set_pending_click_through_flag(pending_click_through.clone());
323329
view
324330
});
325331

@@ -377,6 +383,16 @@ pub fn run_app_with_tray(
377383
TrayCommand::StopLive(room_id) => {
378384
let _ = command_tx_clone.send(UiCommand::StopLive { room_id });
379385
}
386+
TrayCommand::ToggleClickThrough => {
387+
let click_through = tray_mgr.lock().toggle_click_through();
388+
pending_click_through_for_spawn.store(true, std::sync::atomic::Ordering::Relaxed);
389+
let _ = cx.update(|cx| {
390+
let _ = cx.update_window(window_handle, |_, window, _cx| {
391+
crate::platform::set_window_click_through(window, click_through);
392+
});
393+
cx.refresh_windows();
394+
});
395+
}
380396
TrayCommand::Quit => {
381397
let _ = cx.update(|cx| {
382398
cx.quit();

crates/jlivertool-ui/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub mod views;
1313
pub use app::{run_app, run_app_with_plugins, run_app_with_tray, window_bounds_from_config, EventReceiver, UiCommand};
1414
pub use components::QrCodeView;
1515
pub use http::IsahcHttpClient;
16-
pub use platform::{hide_window, is_window_visible, set_window_always_on_top, show_window};
16+
pub use platform::{hide_window, is_window_visible, set_window_always_on_top, set_window_click_through, show_window};
1717
pub use theme::{set_theme, update_gpui_component_theme};
1818
pub use tray::{TrayCommand, TrayManager, TrayState};
1919
pub use views::setting_view::PluginInfo;

crates/jlivertool-ui/src/platform.rs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,42 @@ mod macos {
144144
visible == YES
145145
}
146146
}
147+
/// Set window click-through (ignores mouse events)
148+
pub fn set_window_click_through(window: &Window, click_through: bool) {
149+
use objc::runtime::Object;
150+
use objc::{msg_send, sel, sel_impl};
151+
152+
let handle = match HasWindowHandle::window_handle(window) {
153+
Ok(h) => h,
154+
Err(e) => {
155+
tracing::warn!("Failed to get window handle: {:?}", e);
156+
return;
157+
}
158+
};
159+
160+
let raw_window_handle::RawWindowHandle::AppKit(appkit_handle) = handle.as_raw() else {
161+
tracing::warn!("Not an AppKit window handle");
162+
return;
163+
};
164+
165+
let ns_view = appkit_handle.ns_view.as_ptr() as *mut Object;
166+
167+
unsafe {
168+
let ns_window: *mut Object = msg_send![ns_view, window];
169+
if ns_window.is_null() {
170+
tracing::warn!("Failed to get NSWindow from NSView");
171+
return;
172+
}
173+
174+
let ignore: objc::runtime::BOOL = if click_through {
175+
objc::runtime::YES
176+
} else {
177+
objc::runtime::NO
178+
};
179+
let _: () = msg_send![ns_window, setIgnoresMouseEvents: ignore];
180+
tracing::info!("Set window click-through: {}", click_through);
181+
}
182+
}
147183
}
148184

149185
#[cfg(target_os = "windows")]
@@ -253,6 +289,39 @@ mod windows_impl {
253289

254290
unsafe { IsWindowVisible(hwnd).as_bool() }
255291
}
292+
293+
/// Set window click-through (ignores mouse events)
294+
pub fn set_window_click_through(window: &Window, click_through: bool) {
295+
use windows::Win32::UI::WindowsAndMessaging::{
296+
GetWindowLongW, SetWindowLongW, GWL_EXSTYLE, WS_EX_TRANSPARENT, WS_EX_LAYERED,
297+
};
298+
299+
let handle = match HasWindowHandle::window_handle(window) {
300+
Ok(h) => h,
301+
Err(e) => {
302+
tracing::warn!("Failed to get window handle: {:?}", e);
303+
return;
304+
}
305+
};
306+
307+
let raw_window_handle::RawWindowHandle::Win32(win32_handle) = handle.as_raw() else {
308+
tracing::warn!("Not a Win32 window handle");
309+
return;
310+
};
311+
312+
let hwnd = HWND(win32_handle.hwnd.get() as *mut _);
313+
314+
unsafe {
315+
let ex_style = GetWindowLongW(hwnd, GWL_EXSTYLE);
316+
let new_style = if click_through {
317+
ex_style | WS_EX_TRANSPARENT.0 as i32 | WS_EX_LAYERED.0 as i32
318+
} else {
319+
ex_style & !(WS_EX_TRANSPARENT.0 as i32)
320+
};
321+
SetWindowLongW(hwnd, GWL_EXSTYLE, new_style);
322+
tracing::info!("Set window click-through: {}", click_through);
323+
}
324+
}
256325
}
257326

258327
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
@@ -278,13 +347,18 @@ mod other {
278347
pub fn is_window_visible(_window: &Window) -> bool {
279348
true
280349
}
350+
351+
/// Set window click-through (no-op on unsupported platforms)
352+
pub fn set_window_click_through(_window: &Window, _click_through: bool) {
353+
tracing::warn!("Click-through is not supported on this platform");
354+
}
281355
}
282356

283357
#[cfg(target_os = "macos")]
284-
pub use macos::{hide_window, is_window_visible, set_window_always_on_top, show_window};
358+
pub use macos::{hide_window, is_window_visible, set_window_always_on_top, set_window_click_through, show_window};
285359

286360
#[cfg(target_os = "windows")]
287-
pub use windows_impl::{hide_window, is_window_visible, set_window_always_on_top, show_window};
361+
pub use windows_impl::{hide_window, is_window_visible, set_window_always_on_top, set_window_click_through, show_window};
288362

289363
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
290-
pub use other::{hide_window, is_window_visible, set_window_always_on_top, show_window};
364+
pub use other::{hide_window, is_window_visible, set_window_always_on_top, set_window_click_through, show_window};

crates/jlivertool-ui/src/tray/menu.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use muda::{Menu, MenuItem, PredefinedMenuItem};
66
/// Menu item IDs for event handling
77
pub struct MenuIds {
88
pub show_hide: MenuItem,
9+
pub click_through: MenuItem,
910
pub room_info: MenuItem,
1011
pub live_status: MenuItem,
1112
pub start_live: MenuItem,
@@ -27,6 +28,15 @@ pub fn build_menu(state: &TrayState) -> Result<(Menu, MenuIds), Box<dyn std::err
2728
let show_hide = MenuItem::new(show_hide_text, true, None);
2829
menu.append(&show_hide)?;
2930

31+
// Click-through toggle
32+
let click_through_text = if state.click_through {
33+
"关闭鼠标穿透"
34+
} else {
35+
"开启鼠标穿透"
36+
};
37+
let click_through = MenuItem::new(click_through_text, true, None);
38+
menu.append(&click_through)?;
39+
3040
menu.append(&PredefinedMenuItem::separator())?;
3141

3242
// Room Info (disabled label)
@@ -77,6 +87,7 @@ pub fn build_menu(state: &TrayState) -> Result<(Menu, MenuIds), Box<dyn std::err
7787

7888
let menu_ids = MenuIds {
7989
show_hide,
90+
click_through,
8091
room_info,
8192
live_status,
8293
start_live,

crates/jlivertool-ui/src/tray/mod.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ pub enum TrayCommand {
2424
StartLive(u64, u64),
2525
/// Stop live streaming (room_id)
2626
StopLive(u64),
27+
/// Toggle click-through mode
28+
ToggleClickThrough,
2729
/// Quit the application
2830
Quit,
2931
}
@@ -39,6 +41,7 @@ pub struct TrayState {
3941
pub is_room_owner: bool,
4042
pub connected: bool,
4143
pub window_visible: bool,
44+
pub click_through: bool,
4245
}
4346

4447
/// Manages the system tray icon and menu
@@ -87,7 +90,10 @@ impl TrayManager {
8790

8891
/// Update the tray state and refresh the menu
8992
pub fn update_state(&mut self, state: TrayState) {
93+
// Preserve click_through state since it's managed by TrayManager
94+
let click_through = self.state.click_through;
9095
self.state = state.clone();
96+
self.state.click_through = click_through;
9197

9298
// Update menu items based on state
9399
self.update_menu();
@@ -115,6 +121,14 @@ impl TrayManager {
115121
};
116122
self.menu_ids.show_hide.set_text(show_hide_text);
117123

124+
// Update click-through text
125+
let click_through_text = if self.state.click_through {
126+
"关闭鼠标穿透"
127+
} else {
128+
"开启鼠标穿透"
129+
};
130+
self.menu_ids.click_through.set_text(click_through_text);
131+
118132
// Update room info
119133
let room_info = if let Some(room_id) = self.state.room_id {
120134
if self.state.room_title.is_empty() {
@@ -173,6 +187,8 @@ impl TrayManager {
173187
Some(TrayCommand::ToggleWindow)
174188
} else if event.id == self.menu_ids.settings.id() {
175189
Some(TrayCommand::OpenSettings)
190+
} else if event.id == self.menu_ids.click_through.id() {
191+
Some(TrayCommand::ToggleClickThrough)
176192
} else if event.id == self.menu_ids.start_live.id() {
177193
self.state.room_id.map(|room_id| {
178194
TrayCommand::StartLive(room_id, self.state.area_id)
@@ -199,4 +215,16 @@ impl TrayManager {
199215
pub fn command_sender(&self) -> mpsc::Sender<TrayCommand> {
200216
self.command_tx.clone()
201217
}
218+
219+
/// Toggle click-through state and update menu
220+
pub fn toggle_click_through(&mut self) -> bool {
221+
self.state.click_through = !self.state.click_through;
222+
self.update_menu();
223+
self.state.click_through
224+
}
225+
226+
/// Get current click-through state
227+
pub fn click_through_enabled(&self) -> bool {
228+
self.state.click_through
229+
}
202230
}

crates/jlivertool-ui/src/views/main_view/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ pub struct MainView {
100100
always_on_top: bool,
101101
// Flag to apply always_on_top on next render
102102
pending_always_on_top: Option<bool>,
103+
// Click-through mode
104+
click_through: bool,
105+
// Pending click-through change from tray (Arc for thread-safe sharing)
106+
pending_click_through: Arc<AtomicBool>,
103107
// Scroll handle for danmu list
104108
scroll_handle: UniformListScrollHandle,
105109
// Window handles for single-instance windows
@@ -476,6 +480,8 @@ impl MainView {
476480
level_effect: false,
477481
always_on_top: false,
478482
pending_always_on_top: None,
483+
click_through: false,
484+
pending_click_through: Arc::new(AtomicBool::new(false)),
479485
scroll_handle: UniformListScrollHandle::new(),
480486
settings_window: None,
481487
gift_window: None,
@@ -587,6 +593,11 @@ impl MainView {
587593
self.pending_open_settings = flag;
588594
}
589595

596+
/// Set the pending_click_through flag (shared with tray command handler)
597+
pub fn set_pending_click_through_flag(&mut self, flag: Arc<AtomicBool>) {
598+
self.pending_click_through = flag;
599+
}
600+
590601
/// Get the pending_open_settings flag for tray command handling
591602
pub fn pending_open_settings_flag(&self) -> Arc<AtomicBool> {
592603
self.pending_open_settings.clone()
@@ -610,6 +621,7 @@ impl MainView {
610621
is_room_owner,
611622
connected: self.connected,
612623
window_visible: true, // We don't track this in MainView currently
624+
click_through: false, // Managed by TrayManager directly
613625
};
614626
tray.lock().update_state(state);
615627

@@ -1074,6 +1086,7 @@ impl MainView {
10741086

10751087
let gift_view = self.gift_view.clone();
10761088
let always_on_top = self.always_on_top;
1089+
let click_through = self.click_through;
10771090
let command_tx = self.command_tx.clone();
10781091

10791092
if let Ok(handle) = cx.open_window(
@@ -1092,6 +1105,9 @@ impl MainView {
10921105
if always_on_top {
10931106
crate::platform::set_window_always_on_top(new_window, true);
10941107
}
1108+
if click_through {
1109+
crate::platform::set_window_click_through(new_window, true);
1110+
}
10951111
let tracker =
10961112
cx.new(|_| WindowBoundsTracker::new(gift_view, WindowType::Gift, command_tx));
10971113
cx.new(|cx| Root::new(tracker, new_window, cx))
@@ -1135,6 +1151,7 @@ impl MainView {
11351151

11361152
let superchat_view = self.superchat_view.clone();
11371153
let always_on_top = self.always_on_top;
1154+
let click_through = self.click_through;
11381155
let command_tx = self.command_tx.clone();
11391156

11401157
if let Ok(handle) = cx.open_window(
@@ -1153,6 +1170,9 @@ impl MainView {
11531170
if always_on_top {
11541171
crate::platform::set_window_always_on_top(new_window, true);
11551172
}
1173+
if click_through {
1174+
crate::platform::set_window_click_through(new_window, true);
1175+
}
11561176
let tracker = cx.new(|_| {
11571177
WindowBoundsTracker::new(superchat_view, WindowType::SuperChat, command_tx)
11581178
});

crates/jlivertool-ui/src/views/main_view/render.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ impl MainView {
562562
}),
563563
)),
564564
)
565-
.child(
565+
.when(!self.lite_mode, |el| el.child(
566566
div()
567567
.relative()
568568
.w_full()
@@ -748,7 +748,7 @@ impl MainView {
748748
),
749749
)
750750
}),
751-
)
751+
))
752752
}
753753

754754
pub(super) fn render_danmu_list(
@@ -807,6 +807,23 @@ impl Render for MainView {
807807
crate::platform::set_window_always_on_top(window, always_on_top);
808808
}
809809

810+
// Check if tray toggled click-through mode
811+
if self.pending_click_through.swap(false, std::sync::atomic::Ordering::Relaxed) {
812+
// Read the actual state from tray manager
813+
let click_through = self
814+
.tray_manager
815+
.as_ref()
816+
.map(|t| t.lock().click_through_enabled())
817+
.unwrap_or(false);
818+
self.click_through = click_through;
819+
// Apply to all secondary windows
820+
for handle in [self.gift_window, self.superchat_window].into_iter().flatten() {
821+
let _ = cx.update_window(handle, |_, win, _cx| {
822+
crate::platform::set_window_click_through(win, click_through);
823+
});
824+
}
825+
}
826+
810827
// Check if tray requested to open settings
811828
// Use cx.spawn to defer window opening to after render completes
812829
// Opening a window during render causes segfault due to GPUI global state modification

0 commit comments

Comments
 (0)