Repository: tauri-apps/wry (language: rust)

Issue:
Incorrect and incosistent window size with multiwebview
**Describe the bug**
A clear and concise description of what the bug is.

**Steps To Reproduce**
```console
git clone https://github.com/tauri-apps/wry
cd wry
cargo run --example multiwebview
```



**Expected behavior**
The sizes should be of the whole window and same as it is in MacOS

**Screenshots**

## Windows (incorrect size)

<img width="600" alt="image" src="https://github.com/tauri-apps/wry/assets/61390950/b71a6333-c462-4606-a179-b439afe227d6">

## MacOS (correct size)

<img width="600" alt="image" src="https://github.com/tauri-apps/wry/assets/61390950/48987d97-daee-49b8-b135-2709f700ac39">



**Platform and Versions (please complete the following information):**
```console
OS: Windows 11
Rustc: 
cargo 1.75.0 (1d8b05cdd 2023-11-20)
rustc 1.75.0 (82e1608df 2023-12-21)
```

**Additional context**
It works well on `MacOS`




Discussion / hints:
Thanks for reporting, I am aware of that issue and I am working on fixing it before next release.
You may be already aware of, but looks like the problem is that in windows the expected size is PysicalSize but in MacOS the expected size is LogicalSize (calculated with scale factor by default in multiwebview example)
So in windows, by removing the `to_logical` in both window setup and resize handler fixes it.

Yeah, I think we will need to pick one or another and do the conversions internally.

Fix patch:
diff --git a/.changes/dpi-crate.md b/.changes/dpi-crate.md
new file mode 100644
index 000000000..79f378a33
--- /dev/null
+++ b/.changes/dpi-crate.md
@@ -0,0 +1,5 @@
+---
+"wry": "minor"
+---
+
+Add `dpi` module which is a re-export of `dpi` crate.
diff --git a/.changes/rect-dpi.md b/.changes/rect-dpi.md
new file mode 100644
index 000000000..1896b05c4
--- /dev/null
+++ b/.changes/rect-dpi.md
@@ -0,0 +1,5 @@
+---
+"wry": "minor"
+---
+
+**Breaking Change**: Removed `x`, `y`, `with` and `height` fields from `Rect` struct and replaced it with `size` and `position` fields.
diff --git a/Cargo.toml b/Cargo.toml
index a45fcb131..016713645 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,7 @@ rustdoc-args = [ "--cfg", "docsrs" ]
 
 [features]
 default = [ "drag-drop", "objc-exception", "protocol", "os-webview" ]
+serde = [ "dpi/serde" ]
 objc-exception = [ "objc/exception" ]
 drag-drop = [ ]
 protocol = [ ]
@@ -54,6 +55,7 @@ once_cell = "1"
 thiserror = "1.0"
 http = "0.2"
 raw-window-handle = { version = "0.6", features = [ "std" ] }
+dpi = "0.1"
 
 [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
 javascriptcore-rs = { version = "=1.1", features = [ "v2_28" ], optional = true }
diff --git a/examples/gtk_multiwebview.rs b/examples/gtk_multiwebview.rs
index e66bad1e5..93078c871 100644
--- a/examples/gtk_multiwebview.rs
+++ b/examples/gtk_multiwebview.rs
@@ -7,6 +7,7 @@ use tao::{
   event_loop::{ControlFlow, EventLoop},
   window::WindowBuilder,
 };
+use wry::dpi::{LogicalPosition, LogicalSize};
 use wry::{Rect, WebViewBuilder};
 
 fn main() -> wry::Result<()> {
@@ -55,37 +56,29 @@ fn main() -> wry::Result<()> {
 
   let webview = create_webview_builder()
     .with_bounds(Rect {
-      x: 0,
-      y: 0,
-      width: size.width / 2,
-      height: size.height / 2,
+      position: LogicalPosition::new(0, 0).into(),
+      size: LogicalSize::new(size.width / 2, size.height / 2).into(),
     })
     .with_url("https://tauri.app")
     .build()?;
-  let webview2 = create_webview_builder()
+  let webview2 = WebViewBuilder::new_as_child(&window)
     .with_bounds(Rect {
-      x: (size.width / 2) as i32,
-      y: 0,
-      width: size.width / 2,
-      height: size.height / 2,
+      position: LogicalPosition::new(size.width / 2, 0).into(),
+      size: LogicalSize::new(size.width / 2, size.height / 2).into(),
     })
     .with_url("https://github.com/tauri-apps/wry")
     .build()?;
-  let webview3 = create_webview_builder()
+  let webview3 = WebViewBuilder::new_as_child(&window)
     .with_bounds(Rect {
-      x: 0,
-      y: (size.height / 2) as i32,
-      width: size.width / 2,
-      height: size.height / 2,
+      position: LogicalPosition::new(0, size.height / 2).into(),
+      size: LogicalSize::new(size.width / 2, size.height / 2).into(),
     })
     .with_url("https://twitter.com/TauriApps")
     .build()?;
-  let webview4 = create_webview_builder()
+  let webview4 = WebViewBuilder::new_as_child(&window)
     .with_bounds(Rect {
-      x: (size.width / 2) as i32,
-      y: (size.height / 2) as i32,
-      width: size.width / 2,
-      height: size.height / 2,
+      position: LogicalPosition::new(size.width / 2, size.height / 2).into(),
+      size: LogicalSize::new(size.width / 2, size.height / 2).into(),
     })
     .with_url("https://google.com")
     .build()?;
@@ -101,34 +94,26 @@ fn main() -> wry::Result<()> {
         let size = size.to_logical::<u32>(window.scale_factor());
         webview
           .set_bounds(Rect {
-            x: 0,
-            y: 0,
-            width: size.width / 2,
-            height: size.height / 2,
+            position: LogicalPosition::new(0, 0).into(),
+            size: LogicalSize::new(size.width / 2, size.height / 2).into(),
           })
           .unwrap();
         webview2
           .set_bounds(Rect {
-            x: (size.width / 2) as i32,
-            y: 0,
-            width: size.width / 2,
-            height: size.height / 2,
+            position: LogicalPosition::new(size.width / 2, 0).into(),
+            size: LogicalSize::new(size.width / 2, size.height / 2).into(),
           })
           .unwrap();
         webview3
           .set_bounds(Rect {
-            x: 0,
-            y: (size.height / 2) as i32,
-            width: size.width / 2,
-            height: size.height / 2,
+            position: LogicalPosition::new(0, size.height / 2).into(),
+            size: LogicalSize::new(size.width / 2, size.height / 2).into(),
           })
           .unwrap();
         webview4
           .set_bounds(Rect {
-            x: (size.width / 2) as i32,
-            y: (size.height / 2) as i32,
-            width: size.width / 2,
-            height: size.height / 2,
+            position: LogicalPosition::new(size.width / 2, size.height / 2).into(),
+            size: LogicalSize::new(size.width / 2, size.height / 2).into(),
           })
           .unwrap();
       }
diff --git a/examples/multiwebview.rs b/examples/multiwebview.rs
index 0dfe5a96b..569ef7594 100644
--- a/examples/multiwebview.rs
+++ b/examples/multiwebview.rs
@@ -3,11 +3,11 @@
 // SPDX-License-Identifier: MIT
 
 use winit::{
-  dpi::LogicalSize,
   event::{Event, WindowEvent},
   event_loop::{ControlFlow, EventLoop},
   window::WindowBuilder,
 };
+use wry::dpi::{LogicalPosition, LogicalSize};
 use wry::{Rect, WebViewBuilder};
 
 fn main() -> wry::Result<()> {
@@ -36,7 +36,7 @@ fn main() -> wry::Result<()> {
 
   let event_loop = EventLoop::new().unwrap();
   let window = WindowBuilder::new()
-    .with_inner_size(LogicalSize::new(800, 800))
+    .with_inner_size(winit::dpi::LogicalSize::new(800, 800))
     .build(&event_loop)
     .unwrap();
 
@@ -44,37 +44,29 @@ fn main() -> wry::Result<()> {
 
   let webview = WebViewBuilder::new_as_child(&window)
     .with_bounds(Rect {
-      x: 0,
-      y: 0,
-      width: size.width / 2,
-      height: size.height / 2,
+      position: LogicalPosition::new(0, 0).into(),
+      size: LogicalSize::new(size.width / 2, size.height / 2).into(),
     })
     .with_url("https://tauri.app")
     .build()?;
   let webview2 = WebViewBuilder::new_as_child(&window)
     .with_bounds(Rect {
-      x: (size.width / 2) as i32,
-      y: 0,
-      width: size.width / 2,
-      height: size.height / 2,
+      position: LogicalPosition::new(size.width / 2, 0).into(),
+      size: LogicalSize::new(size.width / 2, size.height / 2).into(),
     })
     .with_url("https://github.com/tauri-apps/wry")
     .build()?;
   let webview3 = WebViewBuilder::new_as_child(&window)
     .with_bounds(Rect {
-      x: 0,
-      y: (size.height / 2) as i32,
-      width: size.width / 2,
-      height: size.height / 2,
+      position: LogicalPosition::new(0, size.height / 2).into(),
+      size: LogicalSize::new(size.width / 2, size.height / 2).into(),
     })
     .with_url("https://twitter.com/TauriApps")
     .build()?;
   let webview4 = WebViewBuilder::new_as_child(&window)
     .with_bounds(Rect {
-      x: (size.width / 2) as i32,
-      y: (size.height / 2) as i32,
-      width: size.width / 2,
-      height: size.height / 2,
+      position: LogicalPosition::new(size.width / 2, size.height / 2).into(),
+      size: LogicalSize::new(size.width / 2, size.height / 2).into(),
     })
     .with_url("https://google.com")
     .build()?;
@@ -102,34 +94,26 @@ fn main() -> wry::Result<()> {
           let size = size.to_logical::<u32>(window.scale_factor());
           webview
             .set_bounds(Rect {
-              x: 0,
-              y: 0,
-              width: size.width / 2,
-              height: size.height / 2,
+              position: LogicalPosition::new(0, 0).into(),
+              size: LogicalSize::new(size.width / 2, size.height / 2).into(),
             })
             .unwrap();
           webview2
             .set_bounds(Rect {
-              x: (size.width / 2) as i32,
-              y: 0,
-              width: size.width / 2,
-              height: size.height / 2,
+              position: LogicalPosition::new(size.width / 2, 0).into(),
+              size: LogicalSize::new(size.width / 2, size.height / 2).into(),
             })
             .unwrap();
           webview3
             .set_bounds(Rect {
-              x: 0,
-              y: (size.height / 2) as i32,
-              width: size.width / 2,
-              height: size.height / 2,
+              position: LogicalPosition::new(0, size.height / 2).into(),
+              size: LogicalSize::new(size.width / 2, size.height / 2).into(),
             })
             .unwrap();
           webview4
             .set_bounds(Rect {
-              x: (size.width / 2) as i32,
-              y: (size.height / 2) as i32,
-              width: size.width / 2,
-              height: size.height / 2,
+              position: LogicalPosition::new(size.width / 2, size.height / 2).into(),
+              size: LogicalSize::new(size.width / 2, size.height / 2).into(),
             })
             .unwrap();
         }
diff --git a/examples/wgpu.rs b/examples/wgpu.rs
index 204af4da3..9b816f73a 100644
--- a/examples/wgpu.rs
+++ b/examples/wgpu.rs
@@ -4,6 +4,7 @@ use winit::{
   event_loop::{ControlFlow, EventLoop},
   window::Window,
 };
+use wry::dpi::{LogicalPosition, LogicalSize};
 use wry::{Rect, WebViewBuilder};
 
 async fn run(event_loop: EventLoop<()>, window: Window) {
@@ -100,10 +101,8 @@ fn fs_main() -> @location(0) vec4<f32> {
 
   let _webview = WebViewBuilder::new_as_child(&window)
     .with_bounds(Rect {
-      x: 100,
-      y: 100,
-      width: 200,
-      height: 200,
+      position: LogicalPosition::new(100, 100).into(),
+      size: LogicalSize::new(200, 200).into(),
     })
     .with_transparent(true)
     .with_html(
diff --git a/examples/winit.rs b/examples/winit.rs
index 6762bbb14..b67676416 100644
--- a/examples/winit.rs
+++ b/examples/winit.rs
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: MIT
 
 use winit::{
-  dpi::LogicalSize,
   event::{Event, WindowEvent},
   event_loop::{ControlFlow, EventLoop},
   window::WindowBuilder,
@@ -36,7 +35,7 @@ fn main() -> wry::Result<()> {
 
   let event_loop = EventLoop::new().unwrap();
   let window = WindowBuilder::new()
-    .with_inner_size(LogicalSize::new(800, 800))
+    .with_inner_size(winit::dpi::LogicalSize::new(800, 800))
     .build(&event_loop)
     .unwrap();
 
@@ -71,12 +70,12 @@ fn main() -> wry::Result<()> {
           event: WindowEvent::Resized(size),
           ..
         } => {
+          use wry::dpi::{PhysicalPosition, PhysicalSize};
+
           _webview
             .set_bounds(wry::Rect {
-              x: 0,
-              y: 0,
-              width: size.width,
-              height: size.height,
+              position: PhysicalPosition::new(0, 0).into(),
+              size: PhysicalSize::new(size.width, size.height).into(),
             })
             .unwrap();
         }
diff --git a/src/android/mod.rs b/src/android/mod.rs
index b3c1489fe..37d186ce0 100644
--- a/src/android/mod.rs
+++ b/src/android/mod.rs
@@ -354,12 +354,7 @@ impl InnerWebView {
   }
 
   pub fn bounds(&self) -> Result<crate::Rect> {
-    Ok(crate::Rect {
-      x: 0,
-      y: 0,
-      width: 0,
-      height: 0,
-    })
+    Ok(crate::Rect::default())
   }
 
   pub fn set_bounds(&self, _bounds: crate::Rect) -> Result<()> {
diff --git a/src/lib.rs b/src/lib.rs
index e35dd82e1..139f9d91c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -55,7 +55,7 @@
 //! macOS, Windows and Linux (X11 Only).
 //!
 //! ```no_run
-//! # use wry::{WebViewBuilder, raw_window_handle, Rect};
+//! # use wry::{WebViewBuilder, raw_window_handle, Rect, dpi::*};
 //! # use winit::{window::WindowBuilder, event_loop::EventLoop};
 //! let event_loop = EventLoop::new().unwrap();
 //! let window = WindowBuilder::new().build(&event_loop).unwrap();
@@ -63,10 +63,8 @@
 //! let webview = WebViewBuilder::new_as_child(&window)
 //!   .with_url("https://tauri.app")
 //!   .with_bounds(Rect {
-//!     x: 100,
-//!     y: 100,
-//!     width: 200,
-//!     height: 200,
+//!     position: LogicalPosition::new(100, 100).into(),
+//!     size: LogicalSize::new(200, 200).into(),
 //!   })
 //!   .build()
 //!   .unwrap();
@@ -76,7 +74,7 @@
 //! [`WebViewExtUnix::new_gtk`] or [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`].
 //!
 //! ```no_run
-//! # use wry::{WebViewBuilder, raw_window_handle, Rect};
+//! # use wry::{WebViewBuilder, raw_window_handle, Rect, dpi::*};
 //! # use tao::{window::WindowBuilder, event_loop::EventLoop};
 //! # #[cfg(target_os = "linux")]
 //! # use wry::WebViewBuilderExtUnix;
@@ -103,10 +101,8 @@
 //! let webview = builder
 //!   .with_url("https://tauri.app")
 //!   .with_bounds(Rect {
-//!     x: 100,
-//!     y: 100,
-//!     width: 200,
-//!     height: 200,
+//!     position: LogicalPosition::new(100, 100).into(),
+//!     size: LogicalSize::new(200, 200).into(),
 //!   })
 //!   .build()
 //!   .unwrap();
@@ -248,22 +244,28 @@ use std::{borrow::Cow, path::PathBuf, rc::Rc};
 
 use http::{Request, Response};
 
+pub use dpi;
 pub use error::*;
 pub use http;
 pub use proxy::{ProxyConfig, ProxyEndpoint};
 pub use web_context::WebContext;
 
 /// A rectangular region.
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Clone, Copy, Debug)]
 pub struct Rect {
-  /// x coordinate of top left corner
-  pub x: i32,
-  /// y coordinate of top left corner
-  pub y: i32,
-  /// width
-  pub width: u32,
-  /// height
-  pub height: u32,
+  /// Rect position.
+  pub position: dpi::Position,
+  /// Rect size.
+  pub size: dpi::Size,
+}
+
+impl Default for Rect {
+  fn default() -> Self {
+    Self {
+      position: dpi::LogicalPosition::new(0, 0).into(),
+      size: dpi::LogicalSize::new(0, 0).into(),
+    }
+  }
 }
 
 /// Resolves a custom protocol [`Request`] asynchronously.
@@ -527,10 +529,8 @@ impl Default for WebViewAttributes {
       proxy_config: None,
       focused: true,
       bounds: Some(Rect {
-        x: 0,
-        y: 0,
-        width: 200,
-        height: 200,
+        position: dpi::LogicalPosition::new(0, 0).into(),
+        size: dpi::LogicalSize::new(200, 200).into(),
       }),
     }
   }
diff --git a/src/webkitgtk/mod.rs b/src/webkitgtk/mod.rs
index 39966bf7a..0369db2ab 100644
--- a/src/webkitgtk/mod.rs
+++ b/src/webkitgtk/mod.rs
@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
+use dpi::{LogicalPosition, LogicalSize};
 use gdkx11::{
   ffi::{gdk_x11_window_foreign_new_for_display, GdkX11Display},
   X11Display,
@@ -158,22 +159,23 @@ impl InnerWebView {
     parent: c_ulong,
     attributes: &WebViewAttributes,
   ) -> c_ulong {
-    let window = unsafe {
-      (xlib.XCreateSimpleWindow)(
-        display,
-        parent,
-        attributes.bounds.map(|p| p.x).unwrap_or(0),
-        attributes.bounds.map(|p| p.y).unwrap_or(0),
-        // it is unlikey that bounds are not set because
-        // we have a default for it, but anyways we need to have a fallback
-        // and we need to use 1 not 0 here otherwise xlib will crash
-        attributes.bounds.map(|s| s.width).unwrap_or(1),
-        attributes.bounds.map(|s| s.height).unwrap_or(1),
-        0,
-        0,
-        0,
-      )
-    };
+    let scale_factor = scale_factor_from_x11(xlib, display, parent);
+    let (x, y) = attributes
+      .bounds
+      .map(|b| b.position.to_physical::<f64>(scale_factor))
+      .map(Into::into)
+      .unwrap_or((0, 0));
+    let (width, height) = attributes
+      .bounds
+      .map(|b| b.size.to_physical::<u32>(scale_factor))
+      .map(Into::into)
+      // it is unlikey that bounds are not set because
+      // we have a default for it, but anyways we need to have a fallback
+      // and we need to use 1 not 0 here otherwise xlib will crash
+      .unwrap_or((1, 1));
+
+    let window =
+      unsafe { (xlib.XCreateSimpleWindow)(display, parent, x, y, width, height, 0, 0, 0) };
 
     if attributes.visible {
       unsafe { (xlib.XMapWindow)(display, window) };
@@ -480,16 +482,24 @@ impl InnerWebView {
         .unwrap()
         .pack_start(webview, true, true, 0);
     } else if container_type == "GtkFixed" {
-      webview.set_size_request(
-        attributes.bounds.map(|s| s.width).unwrap_or(1) as i32,
-        attributes.bounds.map(|s| s.height).unwrap_or(1) as i32,
-      );
+      let scale_factor = webview.scale_factor() as f64;
+      let (width, height) = attributes
+        .bounds
+        .map(|b| b.size.to_logical::<i32>(scale_factor))
+        .map(Into::into)
+        .unwrap_or((1, 1));
+      let (x, y) = attributes
+        .bounds
+        .map(|b| b.position.to_logical::<i32>(scale_factor))
+        .map(Into::into)
+        .unwrap_or((0, 0));
+
+      webview.set_size_request(width, height);
 
-      container.dynamic_cast_ref::<gtk::Fixed>().unwrap().put(
-        webview,
-        attributes.bounds.map(|p| p.x).unwrap_or(0),
-        attributes.bounds.map(|p| p.y).unwrap_or(0),
-      );
+      container
+        .dynamic_cast_ref::<gtk::Fixed>()
+        .unwrap()
+        .put(webview, x, y);
 
       is_in_fixed_parent = true;
     } else {
@@ -692,43 +702,36 @@ impl InnerWebView {
         );
 
         if ok != 0 {
-          bounds.x = attributes.x;
-          bounds.y = attributes.y;
-          bounds.width = attributes.width as u32;
-          bounds.height = attributes.height as u32;
+          bounds.position = LogicalPosition::new(attributes.x, attributes.y).into();
+          bounds.size = LogicalSize::new(attributes.width, attributes.height).into();
         }
       }
     } else {
       let (size, _) = self.webview.allocated_size();
-      bounds.width = size.width() as u32;
-      bounds.height = size.height() as u32;
+      bounds.size = LogicalSize::new(size.width(), size.height()).into();
     }
 
     Ok(bounds)
   }
 
   pub fn set_bounds(&self, bounds: Rect) -> Result<()> {
+    let scale_factor = self.webview.scale_factor() as f64;
+    let (width, height) = bounds.size.to_logical::<i32>(scale_factor).into();
+    let (x, y) = bounds.position.to_logical::<i32>(scale_factor).into();
+
     if let Some(x11_data) = &self.x11 {
       let window = &x11_data.gtk_window;
-      window.move_(bounds.x, bounds.y);
+      window.move_(x, y);
       if let Some(window) = window.window() {
-        window.resize(bounds.width as i32, bounds.height as i32);
+        window.resize(width, height);
       }
-      window.size_allocate(&gtk::Allocation::new(
-        0,
-        0,
-        bounds.width as i32,
-        bounds.height as i32,
-      ));
+      window.size_allocate(&gtk::Allocation::new(0, 0, width, height));
     }
 
     if self.is_in_fixed_parent {
-      self.webview.size_allocate(&gtk::Allocation::new(
-        bounds.x,
-        bounds.y,
-        bounds.width as i32,
-        bounds.height as i32,
-      ));
+      self
+        .webview
+        .size_allocate(&gtk::Allocation::new(x, y, width, height));
     }
 
     Ok(())
@@ -825,3 +828,11 @@ struct SendEnteredSpan(tracing::span::EnteredSpan);
 
 #[cfg(feature = "tracing")]
 unsafe impl Send for SendEnteredSpan {}
+
+const BASE_DPI: f64 = 96.0;
+fn scale_factor_from_x11(xlib: &Xlib, display: *mut _XDisplay, parent: c_ulong) -> f64 {
+  let mut attrs = unsafe { std::mem::zeroed() };
+  unsafe { (xlib.XGetWindowAttributes)(display, parent, &mut attrs) };
+  let scale_factor = unsafe { (*attrs.screen).width as f64 * 25.4 / (*attrs.screen).mwidth as f64 };
+  scale_factor / BASE_DPI
+}
diff --git a/src/webview2/mod.rs b/src/webview2/mod.rs
index d60c59735..c27758a09 100644
--- a/src/webview2/mod.rs
+++ b/src/webview2/mod.rs
@@ -3,18 +3,19 @@
 // SPDX-License-Identifier: MIT
 
 mod drag_drop;
+mod util;
 
 use std::{
-  borrow::Cow, cell::RefCell, collections::HashSet, fmt::Write, iter::once,
-  os::windows::prelude::OsStrExt, path::PathBuf, rc::Rc, sync::mpsc,
+  borrow::Cow, cell::RefCell, collections::HashSet, fmt::Write, path::PathBuf, rc::Rc, sync::mpsc,
 };
 
+use dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize};
 use http::{Request, Response as HttpResponse, StatusCode};
 use once_cell::sync::Lazy;
 use raw_window_handle::{HasWindowHandle, RawWindowHandle};
 use webview2_com::{Microsoft::Web::WebView2::Win32::*, *};
 use windows::{
-  core::{s, Interface, PCWSTR, PWSTR},
+  core::{s, w, Interface, HSTRING, PCWSTR, PWSTR},
   Win32::{
     Foundation::*,
     Globalization::{self, MAX_LOCALE_NAME},
@@ -124,7 +125,7 @@ impl InnerWebView {
   ) -> Result<Self> {
     let _ = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
 
-    let (hwnd, bounds) = Self::create_container_hwnd(parent, &attributes, is_child)?;
+    let (hwnd, size) = Self::create_container_hwnd(parent, &attributes, is_child)?;
 
     let drop_handler = attributes.drag_drop_handler.take();
 
@@ -133,7 +134,7 @@ impl InnerWebView {
     let webview = Self::init_webview(
       parent,
       hwnd,
-      bounds,
+      size,
       attributes,
       &env,
       &controller,
@@ -159,7 +160,7 @@ impl InnerWebView {
     parent: HWND,
     attributes: &WebViewAttributes,
     is_child: bool,
-  ) -> Result<(HWND, Rect)> {
+  ) -> Result<(HWND, (i32, i32))> {
     unsafe extern "system" fn default_window_proc(
       hwnd: HWND,
       msg: u32,
@@ -169,7 +170,7 @@ impl InnerWebView {
       DefWindowProcW(hwnd, msg, wparam, lparam)
     }
 
-    let class_name = encode_wide("WRY_WEBVIEW");
+    let class_name = w!("WRY_WEBVIEW");
 
     let class = WNDCLASSEXW {
       cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
@@ -182,7 +183,7 @@ impl InnerWebView {
       hCursor: HCURSOR::default(),
       hbrBackground: HBRUSH::default(),
       lpszMenuName: PCWSTR::null(),
-      lpszClassName: PCWSTR::from_raw(class_name.as_ptr()),
+      lpszClassName: class_name,
       hIconSm: HICON::default(),
     };
 
@@ -193,17 +194,21 @@ impl InnerWebView {
       window_styles |= WS_VISIBLE;
     }
 
+    let dpi = unsafe { util::hwnd_dpi(parent) };
+    let scale_factor = util::dpi_to_scale_factor(dpi);
+
     let (x, y, width, height) = if is_child {
-      let x = attributes.bounds.map(|a| a.x).unwrap_or(CW_USEDEFAULT);
-      let y = attributes.bounds.map(|a| a.y).unwrap_or(CW_USEDEFAULT);
-      let width = attributes
+      let (x, y) = attributes
         .bounds
-        .map(|a| a.width as i32)
-        .unwrap_or(CW_USEDEFAULT);
-      let height = attributes
+        .map(|b| b.position.to_physical::<f64>(scale_factor))
+        .map(Into::into)
+        .unwrap_or((CW_USEDEFAULT, CW_USEDEFAULT));
+      let (width, height) = attributes
         .bounds
-        .map(|a| a.height as i32)
-        .unwrap_or(CW_USEDEFAULT);
+        .map(|b| b.size.to_physical::<u32>(scale_factor))
+        .map(Into::into)
+        .unwrap_or((CW_USEDEFAULT, CW_USEDEFAULT));
+
       (x, y, width, height)
     } else {
       let mut rect = RECT::default();
@@ -230,15 +235,7 @@ impl InnerWebView {
       )
     };
 
-    Ok((
-      hwnd,
-      Rect {
-        x,
-        y,
-        width: width as u32,
-        height: height as u32,
-      },
-    ))
+    Ok((hwnd, (width, height)))
   }
 
   #[inline]
@@ -283,10 +280,10 @@ impl InnerWebView {
 
       arguments
     });
-    let additional_browser_args = encode_wide(additional_browser_args);
+    let additional_browser_args = HSTRING::from(additional_browser_args);
     let additional_browser_args = PCWSTR::from_raw(additional_browser_args.as_ptr());
 
-    let data_directory = data_directory.map(encode_wide);
+    let data_directory = data_directory.map(HSTRING::from);
     let data_directory = data_directory.map(|d| PCWSTR::from_raw(d.as_ptr()));
 
     let (tx, rx) = mpsc::channel();
@@ -357,7 +354,7 @@ impl InnerWebView {
   fn init_webview(
     parent: HWND,
     hwnd: HWND,
-    bounds: Rect,
+    bounds: (i32, i32),
     mut attributes: WebViewAttributes,
     env: &ICoreWebView2Environment,
     controller: &ICoreWebView2Controller,
@@ -457,11 +454,11 @@ impl InnerWebView {
       if let Some(headers) = attributes.headers {
         load_url_with_headers(&webview, env, &url, headers)?;
       } else {
-        let url = encode_wide(url);
+        let url = HSTRING::from(url);
         unsafe { webview.Navigate(PCWSTR::from_raw(url.as_ptr()))? };
       }
     } else if let Some(html) = attributes.html {
-      let html = encode_wide(html);
+      let html = HSTRING::from(html);
       unsafe { webview.NavigateToString(PCWSTR::from_raw(html.as_ptr()))? };
     }
 
@@ -474,8 +471,8 @@ impl InnerWebView {
       controller.SetBounds(RECT {
         left: 0,
         top: 0,
-        right: bounds.x + bounds.width as i32,
-        bottom: bounds.y + bounds.height as i32,
+        right: bounds.0,
+        bottom: bounds.1,
       })?;
 
       controller.SetIsVisible(attributes.visible)?;
@@ -502,7 +499,7 @@ impl InnerWebView {
 
     if let Some(user_agent) = &attributes.user_agent {
       let settings2: ICoreWebView2Settings2 = webview.Settings()?.cast()?;
-      let user_agent = encode_wide(user_agent);
+      let user_agent = HSTRING::from(user_agent);
       settings2.SetUserAgent(PCWSTR::from_raw(user_agent.as_ptr()))?;
     }
 
@@ -698,7 +695,7 @@ impl InnerWebView {
 
             if download_started_handler(uri, &mut path) {
               let simplified = dunce::simplified(&path);
-              let path = encode_wide(simplified.as_os_str());
+              let path = HSTRING::from(simplified.as_os_str());
               let result_file_path = PCWSTR::from_raw(path.as_ptr());
               args.SetResultFilePath(result_file_path)?;
               args.SetHandled(true)?;
@@ -772,8 +769,9 @@ impl InnerWebView {
     for (name, _) in &attributes.custom_protocols {
       // WebView2 supports non-standard protocols only on Windows 10+, so we have to use this workaround
       // See https://github.com/MicrosoftEdge/WebView2Feedback/issues/73
+      let filter = HSTRING::from(format!("{scheme}://{name}.*"));
       webview.AddWebResourceRequestedFilter(
-        PCWSTR::from_raw(encode_wide(format!("{scheme}://{name}.*")).as_ptr()),
+        PCWSTR::from_raw(filter.as_ptr()),
         COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL,
       )?;
     }
@@ -939,7 +937,7 @@ impl InnerWebView {
 
     let status = sent_response.status();
     let status_code = status.as_u16();
-    let status = encode_wide(status.canonical_reason().unwrap_or("OK"));
+    let status = HSTRING::from(status.canonical_reason().unwrap_or("OK"));
 
     let mut headers_map = String::new();
     for (name, value) in sent_response.headers().iter() {
@@ -948,7 +946,7 @@ impl InnerWebView {
         let _ = writeln!(headers_map, "{}: {}", header_key, value);
       }
     }
-    let headers_map = encode_wide(headers_map);
+    let headers_map = HSTRING::from(headers_map);
 
     let mut stream = None;
     if !content.is_empty() {
@@ -970,8 +968,8 @@ impl InnerWebView {
   ) -> windows::core::Result<ICoreWebView2WebResourceResponse> {
     let status = StatusCode::BAD_REQUEST;
     let status_code = status.as_u16();
-    let status = encode_wide(status.canonical_reason().unwrap_or("Bad Request"));
-    let error = encode_wide(err.to_string());
+    let status = HSTRING::from(status.canonical_reason().unwrap_or("Bad Request"));
+    let error = HSTRING::from(err.to_string());
     env.CreateWebResourceResponse(
       None,
       status_code as i32,
@@ -1114,7 +1112,7 @@ impl InnerWebView {
     let webview = webview.clone();
     AddScriptToExecuteOnDocumentCreatedCompletedHandler::wait_for_async_operation(
       Box::new(move |handler| unsafe {
-        let js = encode_wide(js);
+        let js = HSTRING::from(js);
         let js = PCWSTR::from_raw(js.as_ptr());
         webview
           .AddScriptToExecuteOnDocumentCreated(js, &handler)
@@ -1134,8 +1132,9 @@ impl InnerWebView {
     unsafe {
       #[cfg(feature = "tracing")]
       let span = tracing::debug_span!("wry::eval").entered();
+      let js = HSTRING::from(js);
       webview.ExecuteScript(
-        PCWSTR::from_raw(encode_wide(js).as_ptr()),
+        PCWSTR::from_raw(js.as_ptr()),
         &ExecuteScriptCompletedHandler::create(Box::new(|_, res| {
           #[cfg(feature = "tracing")]
           drop(span);
@@ -1178,7 +1177,7 @@ impl InnerWebView {
   }
 
   pub fn load_url(&self, url: &str) -> Result<()> {
-    let url = encode_wide(url);
+    let url = HSTRING::from(url);
     unsafe { self.webview.Navigate(PCWSTR::from_raw(url.as_ptr())) }.map_err(Into::into)
   }
 
@@ -1199,36 +1198,45 @@ impl InnerWebView {
       }];
       unsafe { MapWindowPoints(self.hwnd, *self.parent.borrow(), position_point) };
 
-      bounds.x = position_point[0].x;
-      bounds.y = position_point[0].y;
-      bounds.width = (rect.right - rect.left) as u32;
-      bounds.height = (rect.bottom - rect.top) as u32;
+      bounds.position = LogicalPosition::new(position_point[0].x, position_point[0].y).into();
+      bounds.size = LogicalSize::new(
+        (rect.right - rect.left) as u32,
+        (rect.bottom - rect.top) as u32,
+      )
+      .into();
     } else {
       let mut rect = RECT::default();
       unsafe { self.controller.Bounds(&mut rect) }?;
-      bounds.width = (rect.right - rect.left) as u32;
-      bounds.height = (rect.bottom - rect.top) as u32;
+      bounds.size = LogicalSize::new(
+        (rect.right - rect.left) as u32,
+        (rect.bottom - rect.top) as u32,
+      )
+      .into();
     }
 
     Ok(bounds)
   }
 
-  pub fn set_bounds(&self, bounds: Rect) -> Result<()> {
+  pub fn set_bounds_inner(
+    &self,
+    size: PhysicalSize<i32>,
+    position: PhysicalPosition<i32>,
+  ) -> Result<()> {
     unsafe {
       self.controller.SetBounds(RECT {
         top: 0,
         left: 0,
-        right: bounds.width as _,
-        bottom: bounds.height as _,
+        right: size.width,
+        bottom: size.height,
       })?;
 
       SetWindowPos(
         self.hwnd,
         HWND::default(),
-        bounds.x,
-        bounds.y,
-        bounds.width as i32,
-        bounds.height as i32,
+        position.x,
+        position.y,
+        size.width,
+        size.height,
         SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOZORDER,
       )?;
     }
@@ -1236,6 +1244,15 @@ impl InnerWebView {
     Ok(())
   }
 
+  pub fn set_bounds(&self, bounds: Rect) -> Result<()> {
+    let dpi = unsafe { util::hwnd_dpi(self.hwnd) };
+    let scale_factor = util::dpi_to_scale_factor(dpi);
+    let size = bounds.size.to_physical::<i32>(scale_factor);
+    let position = bounds.position.to_physical(scale_factor);
+    self.set_bounds_inner(size, position)?;
+    Ok(())
+  }
+
   pub fn set_visible(&self, visible: bool) -> Result<()> {
     unsafe {
       ShowWindow(
@@ -1279,12 +1296,7 @@ impl InnerWebView {
         let width = rect.right - rect.left;
         let height = rect.bottom - rect.top;
 
-        self.set_bounds(Rect {
-          x: 0,
-          y: 0,
-          width: width as u32,
-          height: height as u32,
-        })?;
+        self.set_bounds_inner((width, height).into(), (0, 0).into())?;
       }
     }
 
@@ -1352,7 +1364,7 @@ fn load_url_with_headers(
   url: &str,
   headers: http::HeaderMap,
 ) -> Result<()> {
-  let url = encode_wide(url);
+  let url = HSTRING::from(url);
 
   let headers_map = {
     let mut headers_map = String::new();
@@ -1362,12 +1374,12 @@ fn load_url_with_headers(
         let _ = writeln!(headers_map, "{}: {}", header_key, value);
       }
     }
-    encode_wide(headers_map)
+    HSTRING::from(headers_map)
   };
 
   unsafe {
     let env = env.cast::<ICoreWebView2Environment9>()?;
-    let method = encode_wide("GET");
+    let method = HSTRING::from("GET");
     if let Ok(request) = env.CreateWebResourceRequest(
       PCWSTR::from_raw(url.as_ptr()),
       PCWSTR::from_raw(method.as_ptr()),
@@ -1445,11 +1457,6 @@ fn is_windows_7() -> bool {
   v.major == 6 && v.minor == 1
 }
 
-#[inline]
-fn encode_wide(string: impl AsRef<std::ffi::OsStr>) -> Vec<u16> {
-  string.as_ref().encode_wide().chain(once(0)).collect()
-}
-
 #[cfg(test)]
 mod tests {
   use super::is_custom_protocol_uri;
diff --git a/src/webview2/util.rs b/src/webview2/util.rs
new file mode 100644
index 000000000..dee63624d
--- /dev/null
+++ b/src/webview2/util.rs
@@ -0,0 +1,101 @@
+// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use once_cell::sync::Lazy;
+use windows::{
+  core::{HRESULT, HSTRING, PCSTR, PCWSTR},
+  Win32::{
+    Foundation::{FARPROC, HWND, S_OK},
+    Graphics::Gdi::{
+      GetDC, GetDeviceCaps, MonitorFromWindow, HMONITOR, LOGPIXELSX, MONITOR_DEFAULTTONEAREST,
+    },
+    System::LibraryLoader::{GetProcAddress, LoadLibraryW},
+    UI::{
+      HiDpi::{MDT_EFFECTIVE_DPI, MONITOR_DPI_TYPE},
+      WindowsAndMessaging::IsProcessDPIAware,
+    },
+  },
+};
+
+fn get_function_impl(library: &str, function: &str) -> FARPROC {
+  let library = HSTRING::from(library);
+  assert_eq!(function.chars().last(), Some('\0'));
+
+  // Library names we will use are ASCII so we can use the A version to avoid string conversion.
+  let module = unsafe { LoadLibraryW(PCWSTR::from_raw(library.as_ptr())) }.unwrap_or_default();
+  if module.is_invalid() {
+    return None;
+  }
+
+  unsafe { GetProcAddress(module, PCSTR::from_raw(function.as_ptr())) }
+}
+
+macro_rules! get_function {
+  ($lib:expr, $func:ident) => {
+    crate::webview2::util::get_function_impl($lib, concat!(stringify!($func), '\0'))
+      .map(|f| unsafe { std::mem::transmute::<_, $func>(f) })
+  };
+}
+
+pub type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32;
+pub type GetDpiForMonitor = unsafe extern "system" fn(
+  hmonitor: HMONITOR,
+  dpi_type: MONITOR_DPI_TYPE,
+  dpi_x: *mut u32,
+  dpi_y: *mut u32,
+) -> HRESULT;
+
+static GET_DPI_FOR_WINDOW: Lazy<Option<GetDpiForWindow>> =
+  Lazy::new(|| get_function!("user32.dll", GetDpiForWindow));
+static GET_DPI_FOR_MONITOR: Lazy<Option<GetDpiForMonitor>> =
+  Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor));
+
+pub const BASE_DPI: u32 = 96;
+pub fn dpi_to_scale_factor(dpi: u32) -> f64 {
+  dpi as f64 / BASE_DPI as f64
+}
+
+#[allow(non_snake_case)]
+pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 {
+  if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW {
+    // We are on Windows 10 Anniversary Update (1607) or later.
+    match GetDpiForWindow(hwnd) {
+      0 => BASE_DPI, // 0 is returned if hwnd is invalid
+      #[allow(clippy::unnecessary_cast)]
+      dpi => dpi as u32,
+    }
+  } else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR {
+    // We are on Windows 8.1 or later.
+    let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
+    if monitor.0 == 0 {
+      return BASE_DPI;
+    }
+
+    let mut dpi_x = 0;
+    let mut dpi_y = 0;
+    #[allow(clippy::unnecessary_cast)]
+    if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) == S_OK {
+      dpi_x as u32
+    } else {
+      BASE_DPI
+    }
+  } else {
+    let hdc = GetDC(hwnd);
+    if hdc.is_invalid() {
+      return BASE_DPI;
+    }
+
+    // We are on Vista or later.
+    if IsProcessDPIAware().as_bool() {
+      // If the process is DPI aware, then scaling must be handled by the application using
+      // this DPI value.
+      GetDeviceCaps(hdc, LOGPIXELSX) as u32
+    } else {
+      // If the process is DPI unaware, then scaling is performed by the OS; we thus return
+      // 96 (scale factor 1.0) to prevent the window from being re-scaled by both the
+      // application and the WM.
+      BASE_DPI
+    }
+  }
+}
diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs
index 9a51d86fe..b4cd49181 100644
--- a/src/wkwebview/mod.rs
+++ b/src/wkwebview/mod.rs
@@ -17,6 +17,7 @@ use cocoa::{
   base::{id, nil, NO, YES},
   foundation::{NSDictionary, NSFastEnumeration, NSInteger},
 };
+use dpi::{LogicalPosition, LogicalSize};
 use raw_window_handle::{HasWindowHandle, RawWindowHandle};
 
 use std::{
@@ -417,9 +418,20 @@ impl InnerWebView {
 
       #[cfg(target_os = "macos")]
       {
-        let (x, y) = attributes.bounds.map(|b| (b.x, b.y)).unwrap_or((0, 0));
+        use cocoa::appkit::NSWindow;
+
+        let window: id = msg_send![ns_view, window];
+        let scale_factor = window.backingScaleFactor();
+        let (x, y) = attributes
+          .bounds
+          .map(|b| b.position.to_logical::<f64>(scale_factor))
+          .map(Into::into)
+          .unwrap_or((0, 0));
         let (w, h) = if is_child {
-          attributes.bounds.map(|b| (b.width, b.height))
+          attributes
+            .bounds
+            .map(|b| b.size.to_logical::<u32>(scale_factor))
+            .map(Into::into)
         } else {
           None
         }
@@ -1128,25 +1140,30 @@ r#"Object.defineProperty(window, 'ipc', {
       let webview_frame: CGRect = msg_send![self.webview, frame];
 
       Ok(Rect {
-        x: webview_frame.origin.x as i32,
-        y: (parent_frame.size.height - webview_frame.origin.y - webview_frame.size.height) as i32,
-        width: webview_frame.size.width as u32,
-        height: webview_frame.size.height as u32,
+        position: LogicalPosition::new(
+          webview_frame.origin.x,
+          parent_frame.size.height - webview_frame.origin.y - webview_frame.size.height,
+        )
+        .into(),
+        size: LogicalSize::new(webview_frame.size.width, webview_frame.size.height).into(),
       })
     }
   }
 
-  pub fn set_bounds(&self, bounds: Rect) -> crate::Result<()> {
+  pub fn set_bounds(&self, #[allow(unused)] bounds: Rect) -> crate::Result<()> {
+    #[cfg(target_os = "macos")]
     if self.is_child {
+      use cocoa::appkit::NSWindow;
+
+      let window: id = unsafe { msg_send![self.webview, window] };
+      let scale_factor = unsafe { window.backingScaleFactor() };
+      let (x, y) = bounds.position.to_logical::<f64>(scale_factor).into();
+      let (width, height) = bounds.size.to_logical::<i32>(scale_factor).into();
+
       unsafe {
         let frame = CGRect {
-          origin: window_position(
-            msg_send![self.webview, superview],
-            bounds.x,
-            bounds.y,
-            bounds.height as f64,
-          ),
-          size: CGSize::new(bounds.width as f64, bounds.height as f64),
+          origin: window_position(msg_send![self.webview, superview], x, y, height),
+          size: CGSize::new(width, height),
         };
         let () = msg_send![self.webview, setFrame: frame];
       }


Test patch: