asdf98 commited on
Commit
a5ca9aa
·
verified ·
1 Parent(s): 807cf88

fix: child webview positioning use logical CSS coords and offscreen parking

Browse files
Files changed (1) hide show
  1. src-tauri/src/browser/layout.rs +28 -42
src-tauri/src/browser/layout.rs CHANGED
@@ -1,75 +1,61 @@
1
- use tauri::{AppHandle, LogicalPosition, LogicalSize, Manager, PhysicalPosition, PhysicalSize};
2
 
3
  use super::tab_manager::{tab_label, ViewportLayout};
4
  use crate::state::AppState;
5
 
6
- /// Hidden position far offscreen. WebView2 stays "visible" (avoids deferred init bug)
7
- /// but is not renderable. This is the correct pattern for WebView2 on Windows.
8
- const OFFSCREEN_X: i32 = -32000;
9
- const OFFSCREEN_Y: i32 = -32000;
10
 
11
- /// Calculate child WebView bounds from measured React host rect.
12
- /// React sends getBoundingClientRect() values in CSS/logical pixels.
13
  pub fn bounds(layout: &ViewportLayout) -> (f64, f64, f64, f64) {
14
- let x = layout.x.max(0.0);
15
- let y = layout.y.max(0.0);
16
- let w = layout.width.max(1.0);
17
- let h = layout.height.max(1.0);
18
- (x, y, w, h)
 
19
  }
20
 
21
- /// Show a tab webview at the specified layout position.
22
- /// Uses PhysicalPosition with scale factor for correct HiDPI positioning.
 
 
 
 
 
 
23
  pub fn show_tab(app: &AppHandle, tab_id: &str, layout: &ViewportLayout) -> Result<(), String> {
24
  let label = tab_label(app, tab_id)?;
25
  let (x, y, w, h) = bounds(layout);
26
-
27
  if let Some(webview) = app.get_webview(&label) {
28
- // Get scale factor for coordinate conversion
29
- let scale = app.get_window("main")
30
- .and_then(|win| win.scale_factor().ok())
31
- .unwrap_or(1.0);
32
-
33
- // Convert CSS logical pixels to physical pixels
34
- let px = (x * scale) as i32;
35
- let py = (y * scale) as i32;
36
- let pw = (w * scale).max(1.0) as u32;
37
- let ph = (h * scale).max(1.0) as u32;
38
-
39
- // CRITICAL ORDER: position + size FIRST, then show
40
- // This avoids the WebView2 "bounds before visible" requirement
41
- webview.set_position(PhysicalPosition::new(px, py)).map_err(|e| e.to_string())?;
42
- webview.set_size(PhysicalSize::new(pw, ph)).map_err(|e| e.to_string())?;
43
-
44
- // show() is needed on first display after add_child
45
- // Subsequent calls are safe (idempotent)
46
  let _ = webview.show();
47
  let _ = webview.set_focus();
48
  }
49
  Ok(())
50
  }
51
 
52
- /// Hide a tab webview by parking it offscreen.
53
- /// We do NOT call webview.hide() because WebView2's hide/show is unreliable.
54
- /// Instead we move it far offscreen and shrink to 1x1. The webview stays "alive"
55
- /// and continues processing JS (beneficial for maintaining session state).
56
  pub fn hide_tab(app: &AppHandle, tab_id: &str) -> Result<(), String> {
57
  let label = tab_label(app, tab_id)?;
58
  if let Some(webview) = app.get_webview(&label) {
59
- // Park offscreen — don't call hide()
60
- webview.set_position(PhysicalPosition::new(OFFSCREEN_X, OFFSCREEN_Y)).map_err(|e| e.to_string())?;
61
- webview.set_size(PhysicalSize::new(1u32, 1u32)).map_err(|e| e.to_string())?;
62
  }
63
  Ok(())
64
  }
65
 
66
- /// Resize the active tab to match current layout.
67
  pub fn resize_active(app: &AppHandle, layout: &ViewportLayout) -> Result<(), String> {
68
  let active = {
69
  let state = app.state::<AppState>();
70
  let tabs = state.tabs.lock().map_err(|_| "lock")?;
71
  tabs.active.clone()
72
  };
73
- if let Some(active_id) = active { show_tab(app, &active_id, layout)?; }
 
 
74
  Ok(())
75
  }
 
1
+ use tauri::{AppHandle, LogicalPosition, LogicalSize, Manager};
2
 
3
  use super::tab_manager::{tab_label, ViewportLayout};
4
  use crate::state::AppState;
5
 
6
+ const OFFSCREEN_X: f64 = -32000.0;
7
+ const OFFSCREEN_Y: f64 = -32000.0;
 
 
8
 
 
 
9
  pub fn bounds(layout: &ViewportLayout) -> (f64, f64, f64, f64) {
10
+ (
11
+ layout.x.max(0.0),
12
+ layout.y.max(0.0),
13
+ layout.width.max(1.0),
14
+ layout.height.max(1.0),
15
+ )
16
  }
17
 
18
+ /// Child webviews added with `window.add_child` are positioned in the parent
19
+ /// window client-area logical coordinate space. DOM getBoundingClientRect()
20
+ /// from the main webview also reports CSS/logical pixels relative to that same
21
+ /// client area (because our main webview fills the undecorated window).
22
+ ///
23
+ /// So: use LogicalPosition/LogicalSize directly. Do NOT multiply by DPI here.
24
+ /// Passing PhysicalPosition can push the child webview out of the intended
25
+ /// panel on scaled displays and was the main reason it appeared invisible.
26
  pub fn show_tab(app: &AppHandle, tab_id: &str, layout: &ViewportLayout) -> Result<(), String> {
27
  let label = tab_label(app, tab_id)?;
28
  let (x, y, w, h) = bounds(layout);
 
29
  if let Some(webview) = app.get_webview(&label) {
30
+ // Bounds first, then visible/focus. Idempotent and cross-platform.
31
+ webview.set_position(LogicalPosition::new(x, y)).map_err(|e| e.to_string())?;
32
+ webview.set_size(LogicalSize::new(w, h)).map_err(|e| e.to_string())?;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  let _ = webview.show();
34
  let _ = webview.set_focus();
35
  }
36
  Ok(())
37
  }
38
 
39
+ /// Park offscreen instead of relying on hide/show lifecycle. This keeps WebView2
40
+ /// initialized and avoids stale rendering after panel close/open.
 
 
41
  pub fn hide_tab(app: &AppHandle, tab_id: &str) -> Result<(), String> {
42
  let label = tab_label(app, tab_id)?;
43
  if let Some(webview) = app.get_webview(&label) {
44
+ webview.set_position(LogicalPosition::new(OFFSCREEN_X, OFFSCREEN_Y)).map_err(|e| e.to_string())?;
45
+ webview.set_size(LogicalSize::new(1.0, 1.0)).map_err(|e| e.to_string())?;
46
+ // Do not call hide(); parking is more reliable with child WebView2.
47
  }
48
  Ok(())
49
  }
50
 
 
51
  pub fn resize_active(app: &AppHandle, layout: &ViewportLayout) -> Result<(), String> {
52
  let active = {
53
  let state = app.state::<AppState>();
54
  let tabs = state.tabs.lock().map_err(|_| "lock")?;
55
  tabs.active.clone()
56
  };
57
+ if let Some(active_id) = active {
58
+ show_tab(app, &active_id, layout)?;
59
+ }
60
  Ok(())
61
  }