|
| 1 | +--- |
| 2 | +title: "Tauri" |
| 3 | +page: true |
| 4 | +aside: true |
| 5 | +--- |
| 6 | + |
| 7 | +# Tauri |
| 8 | +Tauri is toolkit helping you develop cross-platform desktop GUI app. We'll talk about how Tauri does cross-platform work. |
| 9 | + |
| 10 | +## Window |
| 11 | +There're two questions Tauri has to answer: |
| 12 | +1. how to build window |
| 13 | +2. how to build page inside window |
| 14 | + |
| 15 | +In this chapter, we talk about first question, in next chapter, we talk about the latter. |
| 16 | + |
| 17 | +Keep in mind that if we want to build a window, we have to depend on capbilities of operating system. On macOS, when you develop an app, you normally use `swiftUI` or other frameworks like that. These frameworks are developed by `Objective-C` or `Swift`. But there're ways to leverage frameworks with other languages, like `Rust` which Tauri uses. To reach it, we should take 2 steps: |
| 18 | +1. use `Rust bingdings` for `Objective-C` or `Swift`, when we build a window |
| 19 | +2. link `Objective-C` or `Swift` frameworks when we compile `Rust` code |
| 20 | + |
| 21 | +`winit`, a Rust crate, has already done these work for us. Tauri has its own crate `Tao` which is a fork of `winit` and brings more features like menu and system tray.Note that menu and system tray are not part of window, so `winit` doesn't implement them; In early time, `Tao` implements them, but now move them into other crates, `muda` for menu, `tray-icon` for system tray. |
| 22 | + |
| 23 | +Here're code snippet from `Tao`, exploring how to build window. |
| 24 | + |
| 25 | +On macOS: |
| 26 | +```rs |
| 27 | +use objc2::{ |
| 28 | + msg_send, |
| 29 | + rc::Retained, |
| 30 | + runtime::{AnyClass as Class, AnyObject as Object, ClassBuilder as ClassDecl, Sel}, |
| 31 | +}; |
| 32 | + |
| 33 | +let ns_window = msg_send![WINDOW_CLASS.0, alloc]; |
| 34 | +let ns_window: Option<Retained<NSWindow>> = msg_send![ |
| 35 | + ns_window, |
| 36 | + initWithContentRect: frame, |
| 37 | + styleMask: masks, |
| 38 | + backing: NSBackingStoreType::Buffered, |
| 39 | + defer: NO, |
| 40 | +]; |
| 41 | +``` |
| 42 | +As you can see, these code is based on `Rust bindings` for `Objective-C` and its runtime. `msg_send`, `WINDOW_CLASS.0`, are concepts from `Objective-C`. |
| 43 | + |
| 44 | +On ios: |
| 45 | +```rs |
| 46 | +let frame = match window_attributes.inner_size { |
| 47 | + Some(dim) => { |
| 48 | + let scale_factor = msg_send![screen, scale]; |
| 49 | + let size = dim.to_logical::<f64>(scale_factor); |
| 50 | + CGRect { |
| 51 | + origin: screen_bounds.origin, |
| 52 | + size: CGSize { |
| 53 | + width: size.width as _, |
| 54 | + height: size.height as _, |
| 55 | + }, |
| 56 | + } |
| 57 | + } |
| 58 | + None => screen_bounds, |
| 59 | +}; |
| 60 | + |
| 61 | +let view = view::create_view(&window_attributes, &platform_attributes, frame); |
| 62 | + |
| 63 | +let gl_or_metal_backed = { |
| 64 | + let view_class: *const AnyClass = msg_send![view, class]; |
| 65 | + let layer_class: *const AnyClass = msg_send![view_class, layerClass]; |
| 66 | + let is_metal: bool = msg_send![layer_class, isSubclassOfClass: class!(CAMetalLayer)]; |
| 67 | + let is_gl: bool = msg_send![layer_class, isSubclassOfClass: class!(CAEAGLLayer)]; |
| 68 | + is_metal || is_gl |
| 69 | +}; |
| 70 | + |
| 71 | +let view_controller = |
| 72 | + view::create_view_controller(&window_attributes, &platform_attributes, view); |
| 73 | +let window = view::create_window( |
| 74 | + &window_attributes, |
| 75 | + &platform_attributes, |
| 76 | + frame, |
| 77 | + view_controller, |
| 78 | +); |
| 79 | + |
| 80 | +// requires main thread |
| 81 | +// view::create_window |
| 82 | +pub unsafe fn create_window( |
| 83 | + window_attributes: &WindowAttributes, |
| 84 | + _platform_attributes: &PlatformSpecificWindowBuilderAttributes, |
| 85 | + frame: CGRect, |
| 86 | + view_controller: id, |
| 87 | +) -> id { |
| 88 | + let class = get_window_class(); |
| 89 | + |
| 90 | + let window: id = msg_send![class, alloc]; |
| 91 | + assert!(!window.is_null(), "Failed to create `UIWindow` instance"); |
| 92 | + let window: id = msg_send![window, initWithFrame: frame]; |
| 93 | + assert!( |
| 94 | + !window.is_null(), |
| 95 | + "Failed to initialize `UIWindow` instance" |
| 96 | + ); |
| 97 | + let () = msg_send![window, setRootViewController: view_controller]; |
| 98 | + match window_attributes.fullscreen { |
| 99 | + Some(Fullscreen::Exclusive(ref video_mode)) => { |
| 100 | + let uiscreen = video_mode.monitor().ui_screen() as id; |
| 101 | + let () = msg_send![uiscreen, setCurrentMode: video_mode.video_mode.screen_mode.0]; |
| 102 | + msg_send![window, setScreen:video_mode.monitor().ui_screen()] |
| 103 | + } |
| 104 | + Some(Fullscreen::Borderless(ref monitor)) => { |
| 105 | + let uiscreen: id = match &monitor { |
| 106 | + Some(monitor) => monitor.ui_screen() as id, |
| 107 | + None => { |
| 108 | + let uiscreen: id = msg_send![window, screen]; |
| 109 | + uiscreen |
| 110 | + } |
| 111 | + }; |
| 112 | + |
| 113 | + msg_send![window, setScreen: uiscreen] |
| 114 | + } |
| 115 | + None => (), |
| 116 | + } |
| 117 | + |
| 118 | + window |
| 119 | +} |
| 120 | +``` |
| 121 | +As you can see, ios also depends on `Rust bindings` for `Objective-C`. Here's a question, we only have bindings code, but we also need to link `Objective-C` library at compile time, how to do ? The answer is `objc2` crate helps us. Take a look at `objc2` source code: |
| 122 | +```rs |
| 123 | +// Link to libobjc |
| 124 | +#[cfg_attr(not(feature = "unstable-objfw"), link(name = "objc", kind = "dylib"))] |
| 125 | +// Link to libobjfw-rt |
| 126 | +#[cfg_attr(feature = "unstable-objfw", link(name = "objfw-rt", kind = "dylib"))] |
| 127 | +extern "C" {} |
| 128 | +``` |
| 129 | +When we compile project which depends on `objc2`, Cargo will link `objc` dylib. |
| 130 | + |
| 131 | + |
| 132 | +On Windows: |
| 133 | +```rs |
| 134 | +use windows::{ |
| 135 | + core::PCWSTR, |
| 136 | +} |
| 137 | +let handle = CreateWindowExW( |
| 138 | + ex_style, |
| 139 | + PCWSTR::from_raw(class_name.as_ptr()), |
| 140 | + PCWSTR::from_raw(title.as_ptr()), |
| 141 | + style, |
| 142 | + position.0, |
| 143 | + position.1, |
| 144 | + adjusted_size.0, |
| 145 | + adjusted_size.1, |
| 146 | + parent, |
| 147 | + pl_attribs.menu, |
| 148 | + GetModuleHandleW(PCWSTR::null()).map(Into::into).ok(), |
| 149 | + Some(Box::into_raw(Box::new(window_flags)) as _), |
| 150 | +)?; |
| 151 | +``` |
| 152 | +As you can see, building window on Windows depends on `windows`. `windows` is a crate provides `Rust bindings` for windows API, you're familiar if you use [`c++/winrt`](https://github.com/microsoft/cppwinrt) project, which provides you c++ function to allow you enjoy Windows API. |
| 153 | + |
| 154 | + |
| 155 | + |
| 156 | +On Linux: |
| 157 | +```rs |
| 158 | +use gtk::{ |
| 159 | + gdk::WindowState, |
| 160 | + glib::{self, translate::ToGlibPtr}, |
| 161 | + prelude::*, |
| 162 | + CssProvider, Settings, |
| 163 | +}; |
| 164 | + |
| 165 | +let mut window_builder = gtk::ApplicationWindow::builder() |
| 166 | + .application(app) |
| 167 | + .accept_focus(attributes.focused); |
| 168 | + |
| 169 | +if let Parent::ChildOf(parent) = pl_attribs.parent { |
| 170 | + window_builder = window_builder.transient_for(&parent); |
| 171 | +} |
| 172 | + |
| 173 | +let window = window_builder.build(); |
| 174 | +``` |
| 175 | +As you can see, building window on Linux depends on `GTK`. `gtk` is a crate provides `Rust bindings` for `GTK `, like `objc2`. Unlike macOS and Windows, `GTK` might be not installed out of box on Linux, you have to install `GTK`, `GLib` and `Cairo` development files first. |
| 176 | + |
| 177 | +## Webview |
| 178 | +Since we have known how to build a window, but how do we build page inside window ? To make better use of traditional web development tech, Tauri decides to render page with `webview` . |
| 179 | + |
| 180 | +`webview` is a GUI component loading html,css,javascript like browser. You can focus on frontend development. |
| 181 | + |
| 182 | +Tauri creates a crate `wry`, providing cross-platform `webview`. Like `Tao`, `wry` uses `Rust bindings` for platform standard programming language. |
| 183 | + |
| 184 | +On macOS, `wry` uses [`wkwebview`](https://developer.apple.com/documentation/webkit/wkwebview?language=objc). |
| 185 | +```rs |
| 186 | +#[cfg(target_os = "macos")] |
| 187 | +let webview = { |
| 188 | + let window = ns_view.window().unwrap(); |
| 189 | + |
| 190 | + let scale_factor = window.backingScaleFactor(); |
| 191 | + let (x, y) = attributes |
| 192 | + .bounds |
| 193 | + .map(|b| b.position.to_logical::<f64>(scale_factor)) |
| 194 | + .map(Into::into) |
| 195 | + .unwrap_or((0, 0)); |
| 196 | + let (w, h) = if is_child { |
| 197 | + attributes |
| 198 | + .bounds |
| 199 | + .map(|b| b.size.to_logical::<u32>(scale_factor)) |
| 200 | + .map(Into::into) |
| 201 | + } else { |
| 202 | + None |
| 203 | + } |
| 204 | + .unwrap_or_else(|| { |
| 205 | + if is_child { |
| 206 | + let frame = NSView::frame(ns_view); |
| 207 | + (frame.size.width as u32, frame.size.height as u32) |
| 208 | + } else { |
| 209 | + (0, 0) |
| 210 | + } |
| 211 | + }); |
| 212 | + |
| 213 | + let frame = CGRect { |
| 214 | + origin: if is_child { |
| 215 | + window_position(ns_view, x, y, h as f64) |
| 216 | + } else { |
| 217 | + CGPoint::new(x as f64, (0 - y - h as i32) as f64) |
| 218 | + }, |
| 219 | + size: CGSize::new(w as f64, h as f64), |
| 220 | + }; |
| 221 | + let webview: Retained<WryWebView> = |
| 222 | + objc2::msg_send![super(webview), initWithFrame: frame, configuration: &**config]; |
| 223 | + webview |
| 224 | +}; |
| 225 | + |
| 226 | +#[cfg(target_os = "ios")] |
| 227 | +let webview = { |
| 228 | + let frame = ns_view.frame(); |
| 229 | + let webview: Retained<WryWebView> = |
| 230 | + objc2::msg_send![super(webview), initWithFrame: frame, configuration: &**config]; |
| 231 | + if let Some((red, green, blue, alpha)) = attributes.background_color { |
| 232 | + // This is required first since the webview color is applied too late. |
| 233 | + webview.setOpaque(false); |
| 234 | + |
| 235 | + let color = objc2_ui_kit::UIColor::colorWithRed_green_blue_alpha( |
| 236 | + red as f64 / 255.0, |
| 237 | + green as f64 / 255.0, |
| 238 | + blue as f64 / 255.0, |
| 239 | + alpha as f64 / 255.0, |
| 240 | + ); |
| 241 | + |
| 242 | + if !is_child { |
| 243 | + ns_view.setBackgroundColor(Some(&color)); |
| 244 | + } |
| 245 | + // This has to be monitored as it may clash with isOpaque = true. |
| 246 | + // The webview background color may also applied too late so actually not that useful. |
| 247 | + webview.setBackgroundColor(Some(&color)); |
| 248 | + } |
| 249 | + webview |
| 250 | +}; |
| 251 | +``` |
| 252 | + |
| 253 | +On windows, `wry` uses `webview2` which is wrapped by crate `webview2-com`. |
| 254 | + |
| 255 | +On linux, `wry` uses `webkitgtk` which is wrapped by crate `webkit2gtk`. Read more about GTK `webkitview` from [this](https://webkitgtk.org/reference/webkitgtk/stable/ctor.WebView.new.html). |
| 256 | + |
| 257 | + |
0 commit comments