@@ -31,13 +31,17 @@ pub const FLOAT_STATE_EVENT: &str = "float:state";
3131/// 浮窗 UI 状态(Rust 侧单一来源;set_ignore_cursor_events 无 getter 必须自存)。
3232#[ derive( Debug , Clone , Copy , Default ) ]
3333pub struct FloatUi {
34+ /// 是否**显示**(open 语义)。预创建常驻后窗口对象恒存在,
35+ /// 不能用 get_webview_window 存在性判定 open——否则按钮语义恒为
36+ /// "收起"且永远无法重新打开(审查 P0,v0.12.3 审查即修)。
37+ pub open : bool ,
3438 /// 点击穿透(锁定后浮窗不可点,主窗按钮/快捷键解锁)
3539 pub locked : bool ,
3640 /// 是否置顶
3741 pub topmost : bool ,
3842}
3943
40- /// 对外状态视图(camelCase 契约——前端按钮语义数据源)。
44+ /// 对外状态视图(camelCase 契约——前端按钮语义数据源;纯函数构造可单测 )。
4145#[ derive( Debug , Clone , Copy , serde:: Serialize ) ]
4246#[ serde( rename_all = "camelCase" ) ]
4347pub struct FloatUiView {
@@ -46,16 +50,16 @@ pub struct FloatUiView {
4650 pub topmost : bool ,
4751}
4852
49- fn view ( app : & tauri :: AppHandle , ui : & FloatUi ) -> FloatUiView {
53+ fn view ( ui : & FloatUi ) -> FloatUiView {
5054 FloatUiView {
51- open : app . get_webview_window ( CAPTURE_FLOAT_LABEL ) . is_some ( ) ,
55+ open : ui . open ,
5256 locked : ui. locked ,
5357 topmost : ui. topmost ,
5458 }
5559}
5660
5761fn emit_float_state ( app : & tauri:: AppHandle , ui : & FloatUi ) {
58- let _ = app. emit ( FLOAT_STATE_EVENT , view ( app , ui) ) ;
62+ let _ = app. emit ( FLOAT_STATE_EVENT , view ( ui) ) ;
5963}
6064
6165/// setup 预创建隐藏浮窗(P2-10:常驻秒开;失败回落为打开时懒创建——不阻断启动)。
@@ -100,20 +104,28 @@ pub async fn open_capture_float(
100104 app : tauri:: AppHandle ,
101105 state : State < ' _ , AppState > ,
102106) -> Result < ( ) , String > {
103- let ui = * state
107+ let topmost = state
104108 . float_ui
105109 . lock ( )
106- . map_err ( |e| format ! ( "浮窗状态锁中毒: {}" , e) ) ?;
110+ . map_err ( |e| format ! ( "浮窗状态锁中毒: {}" , e) ) ?
111+ . topmost ;
107112 let window = match app. get_webview_window ( CAPTURE_FLOAT_LABEL ) {
108113 Some ( w) => w,
109114 None => build_float_window ( & app, false ) ?,
110115 } ;
111116 window. show ( ) . map_err ( |e| format ! ( "显示采集浮窗失败: {}" , e) ) ?;
112117 window
113- . set_always_on_top ( ui . topmost )
118+ . set_always_on_top ( topmost)
114119 . map_err ( |e| format ! ( "设置浮窗置顶失败: {}" , e) ) ?;
115120 let _ = window. set_focus ( ) ;
116- emit_float_state ( & app, & ui) ;
121+ {
122+ let mut ui = state
123+ . float_ui
124+ . lock ( )
125+ . map_err ( |e| format ! ( "浮窗状态锁中毒: {}" , e) ) ?;
126+ ui. open = true ;
127+ emit_float_state ( & app, & ui) ;
128+ }
117129 Ok ( ( ) )
118130}
119131
@@ -131,10 +143,11 @@ pub async fn close_capture_float(
131143 if let Some ( window) = app. get_webview_window ( CAPTURE_FLOAT_LABEL ) {
132144 window. hide ( ) . map_err ( |e| format ! ( "隐藏采集浮窗失败: {}" , e) ) ?;
133145 }
134- let ui = * state
146+ let mut ui = state
135147 . float_ui
136148 . lock ( )
137149 . map_err ( |e| format ! ( "浮窗状态锁中毒: {}" , e) ) ?;
150+ ui. open = false ;
138151 emit_float_state ( & app, & ui) ;
139152 Ok ( ( ) )
140153}
@@ -163,7 +176,7 @@ pub async fn float_set_locked(
163176 . lock ( )
164177 . map_err ( |e| format ! ( "浮窗状态锁中毒: {}" , e) ) ?;
165178 ui. locked = locked;
166- let v = view ( & app , & ui) ;
179+ let v = view ( & ui) ;
167180 emit_float_state ( & app, & ui) ;
168181 Ok ( v)
169182}
@@ -185,19 +198,39 @@ pub async fn float_set_topmost(
185198 . lock ( )
186199 . map_err ( |e| format ! ( "浮窗状态锁中毒: {}" , e) ) ?;
187200 ui. topmost = topmost;
188- let v = view ( & app , & ui) ;
201+ let v = view ( & ui) ;
189202 emit_float_state ( & app, & ui) ;
190203 Ok ( v)
191204}
192205
193206/// 浮窗当前状态(主窗挂载/快捷键语义同步;轻量读,无需 async)。
194207#[ tauri:: command]
195- pub fn float_state ( app : tauri :: AppHandle , state : State < ' _ , AppState > ) -> Result < FloatUiView , String > {
208+ pub fn float_state ( state : State < ' _ , AppState > ) -> Result < FloatUiView , String > {
196209 let ui = * state
197210 . float_ui
198211 . lock ( )
199212 . map_err ( |e| format ! ( "浮窗状态锁中毒: {}" , e) ) ?;
200- Ok ( view ( & app, & ui) )
213+ Ok ( view ( & ui) )
214+ }
215+
216+ #[ cfg( test) ]
217+ mod tests {
218+ use super :: * ;
219+
220+ /// 审查即修(v0.12.3):open 必须是显式状态——常驻预创建后窗口对象恒存在,
221+ /// 不能用 get_webview_window 存在性判定(否则 open 恒 true,主窗按钮
222+ /// 永远显示"收起"且无法再次打开)。
223+ #[ test]
224+ fn float_ui_view_open_is_explicit_state ( ) {
225+ let ui = FloatUi { open : false , locked : true , topmost : false } ;
226+ let v = view ( & ui) ;
227+ let json = serde_json:: to_value ( v) . expect ( "序列化失败" ) ;
228+ assert_eq ! ( json[ "open" ] , false , "open 必须为显式状态(预创建存在≠显示)" ) ;
229+ assert_eq ! ( json[ "locked" ] , true ) ;
230+ assert_eq ! ( json[ "topmost" ] , false ) ;
231+ // camelCase 契约锚定(前端直接消费该 JSON 键)
232+ assert ! ( json. get( "open" ) . is_some( ) && json. get( "locked" ) . is_some( ) && json. get( "topmost" ) . is_some( ) ) ;
233+ }
201234}
202235
203236/// 回主窗:显示 + 还原 + 聚焦主窗口(浮窗保留——回主窗不损失悬浮态)。
0 commit comments