├── .cargo └── config.toml ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── embedded_webview.png └── multi_view.png ├── build.rs ├── examples ├── embedded_webview.rs └── multi_webview.rs └── src ├── engines.rs ├── engines └── ultralight.rs ├── lib.rs ├── webview.rs └── webview ├── advanced.rs └── basic.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # if --features ultralight-resources is not set, 2 | # then you must configure the resources path in you .cargo/config.toml like this 3 | # the resources directory can be found at https://ultralig.ht/download/ 4 | [env] 5 | ULTRALIGHT_RESOURCES_DIR = "./resources" 6 | # ex. 7 | # ULTRALIGHT_RESOURCES_DIR = "/home/user/ultralight_resources/resources" 8 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | default 3 | resources 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "iced_webview" 3 | version = "0.0.5" 4 | edition = "2021" 5 | rust-version = "1.81.0" 6 | description = "An easily embedded webview library for iced" 7 | authors = ["LegitCamper"] 8 | repository = "https://github.com/LegitCamper/iced_webview/" 9 | license = "Apache-2.0" 10 | readme = "README.md" 11 | 12 | [package.metadata.docs.rs] 13 | features = ["docs_only"] 14 | all-features = false 15 | no-default-features = true 16 | 17 | [profile.dev] 18 | incremental = true 19 | opt-level = "s" 20 | lto = "thin" 21 | 22 | [features] 23 | default = ["ultralight"] 24 | ultralight = ["dep:ul-next"] 25 | ultralight-resources = [] 26 | docs_only = [] 27 | 28 | [dependencies] 29 | clipboard-rs = "0.2.1" 30 | iced = { version = "0.13", features = ["advanced", "image", "tokio", "lazy"] } 31 | rand = "0.8.5" 32 | smol_str = "0.2.2" 33 | ul-next = { version = "0.4", optional = true } 34 | url = "2.5.2" 35 | html = "0.6.3" 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Iced_webview [![Rust](https://github.com/LegitCamper/iced_webview/actions/workflows/rust.yml/badge.svg)](https://github.com/LegitCamper/iced_webview/actions/workflows/rust.yml) 2 | 3 | A library to embed Web views in iced applications 4 | 5 | > Note: Currently this library only supports [Ultralight]/Webkit, but more rendering engines are planned to be supported. 6 | 7 | > [Ultralight has its own license](https://ultralig.ht/pricing/) that should be reviewed before deciding if it works for you 8 | 9 | #### examples: 10 | 11 | ##### `examples/embedded_webview` 12 | A simple example to showcase an embedded webview (uses the basic webview) 13 | ![image](https://raw.githubusercontent.com/LegitCamper/iced_webview/refs/heads/main/assets/embedded_webview.png) 14 | ```sh 15 | cargo run --example embedded_webview --features ultralight-resources 16 | ``` 17 | 18 | ##### `examples/multi_webview` 19 | A more advanced example that uses the advanced webview module and has two simultaneous webviews open 20 | ![image](https://raw.githubusercontent.com/LegitCamper/iced_webview/refs/heads/main/assets/multi_view.png) 21 | ```sh 22 | cargo run --example multi_webview --features ultralight-resources 23 | ``` 24 | 25 | ## Extra files (Resources) 26 | 27 | Ultralight requires runtime resources. (cacert.pem, icudt67l.dat) 28 | 29 | > You can either set the path to them with the `ULTRALIGHT_RESOURCES_DIR` env. This varible can also be set in `.cargo/config.toml`. The resouces direcory can be downloaded from [Ultralight SDK] 30 | 31 | > Or Rust will do its best symlink the directory with `--features ultralight-resources`. If this fails please use `ULTRALIGHT_RESOURCES_DIR` 32 | 33 | ## Deployment 34 | 35 | The samples compiled rely on dynamic libraries provided by `Ultralight`: 36 | - `libUltralightCore.so`/`UltralightCore.dll` 37 | - `libUltralight.so`/`Ultralight.dll` 38 | - `libWebCore.so`/`WebCore.dll` 39 | - `libAppCore.so`/`AppCore.dll` 40 | 41 | These can be downloaded from the [Ultralight SDK]. 42 | 43 | > Rust will download them during build as well, but are kept inside the `target` directory. 44 | 45 | [Ultralight]: https://ultralig.ht 46 | [Ultralight SDK]: https://ultralig.ht/download/ 47 | -------------------------------------------------------------------------------- /assets/embedded_webview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LegitCamper/iced_webview/33d14477baa0136375034846584fff99ca36d897/assets/embedded_webview.png -------------------------------------------------------------------------------- /assets/multi_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LegitCamper/iced_webview/33d14477baa0136375034846584fff99ca36d897/assets/multi_view.png -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "ultralight-resources")] 2 | use std::env::var; 3 | #[cfg(feature = "ultralight-resources")] 4 | use std::fs::{self, DirEntry}; 5 | #[cfg(feature = "ultralight-resources")] 6 | use std::path::Path; 7 | 8 | fn main() { 9 | #[cfg(not(feature = "docs_only"))] 10 | { 11 | // ensure runtime resources exist - for examples & local tests 12 | #[cfg(not(feature = "ultralight-resources"))] 13 | std::env::var("ULTRALIGHT_RESOURCES_DIR").expect("If `--features ultralight-resources` is not passed, `ULTRALIGHT_RESOURCES_DIR` Must be set. \nSee README.md for more information"); 14 | #[cfg(feature = "ultralight-resources")] 15 | { 16 | let mut possible_directories = Vec::new(); 17 | let out = var("OUT_DIR").unwrap(); 18 | // This allows it to work in this project but also other projects too 19 | let path = Path::new(&out) 20 | .parent() 21 | .unwrap() 22 | .parent() 23 | .unwrap() 24 | .parent() 25 | .unwrap() 26 | .parent() 27 | .unwrap() 28 | .parent() 29 | .unwrap(); 30 | 31 | let target = Path::new(path).join("target"); 32 | let debug_path = target.clone().join("debug"); 33 | let release_path = target.clone().join("release"); 34 | 35 | if let Ok(debug) = fs::exists(debug_path.clone()) { 36 | if debug { 37 | get_paths( 38 | &mut possible_directories, 39 | debug_path.join("build").to_str().unwrap().to_string(), 40 | ) 41 | } 42 | } else if let Ok(release) = fs::exists(release_path.clone()) { 43 | if release { 44 | get_paths( 45 | &mut possible_directories, 46 | release_path.join("build").to_str().unwrap().to_string(), 47 | ) 48 | } 49 | } else { 50 | panic!("Could not find either debug or release dirs") 51 | } 52 | 53 | assert!(!possible_directories.is_empty()); 54 | 55 | let local_resources = Path::new(path).join("resources"); 56 | 57 | for path in possible_directories { 58 | let resources_dir = path.path().join("out/ul-sdk/resources"); 59 | if let Ok(resources) = fs::exists(resources_dir.clone()) { 60 | if resources { 61 | if let Ok(local_resources_exist) = fs::exists(local_resources.clone()) { 62 | if local_resources_exist { 63 | fs::remove_file(local_resources.clone()) 64 | .expect("Failed to delete resources dir") 65 | } 66 | } 67 | 68 | #[cfg(unix)] 69 | { 70 | std::os::unix::fs::symlink(resources_dir, local_resources) 71 | .expect("Failed to sym link resource dir") 72 | } 73 | 74 | break; 75 | } 76 | } else { 77 | panic!("The resouce dir entered has not resources") 78 | } 79 | } 80 | } 81 | 82 | println!("cargo:rerun-if-changed=target"); 83 | println!("cargo:rerun-if-changed=build.rs"); 84 | println!("cargo:rerun-if-changed=Cargo.lock"); 85 | } 86 | } 87 | 88 | #[cfg(feature = "ultralight-resources")] 89 | fn get_paths(possible_paths: &mut Vec, path_str: String) { 90 | let mut paths: Vec = fs::read_dir(path_str) 91 | .expect("Could not read dir") 92 | .map(|f| f.unwrap()) 93 | .filter(|file| file.path().to_string_lossy().contains("ul-next-sys")) 94 | .collect(); 95 | // TODO: check if sort working 96 | paths.sort_by(|a, b| { 97 | a.metadata() 98 | .unwrap() 99 | .modified() 100 | .unwrap() 101 | .cmp(&b.metadata().unwrap().modified().unwrap()) 102 | }); 103 | possible_paths.append(&mut paths); 104 | } 105 | -------------------------------------------------------------------------------- /examples/embedded_webview.rs: -------------------------------------------------------------------------------- 1 | use iced::{ 2 | time, 3 | widget::{button, column, container, row, text}, 4 | Element, Length, Subscription, Task, 5 | }; 6 | use iced_webview::{Action, PageType, Ultralight, WebView}; 7 | use std::time::Duration; 8 | 9 | static URL: &'static str = "https://docs.rs/iced/latest/iced/index.html"; 10 | 11 | fn main() -> iced::Result { 12 | iced::application("An embedded web view", App::update, App::view) 13 | .subscription(App::subscription) 14 | .run_with(App::new) 15 | } 16 | 17 | #[derive(Debug, Clone)] 18 | enum Message { 19 | WebView(Action), 20 | ToggleWebview, 21 | UrlChanged(String), 22 | WebviewCreated, 23 | CreateWebview, 24 | CycleWebview, 25 | } 26 | 27 | struct App { 28 | webview: WebView, 29 | show_webview: bool, 30 | webview_url: Option, 31 | num_views: u32, 32 | current_view: Option, 33 | } 34 | 35 | impl App { 36 | fn new() -> (Self, Task) { 37 | let webview = WebView::new() 38 | .on_create_view(Message::WebviewCreated) 39 | .on_url_change(Message::UrlChanged); 40 | ( 41 | Self { 42 | webview, 43 | show_webview: false, 44 | webview_url: None, 45 | num_views: 0, 46 | current_view: None, 47 | }, 48 | // Create the first webview so its available once toggled 49 | Task::done(Message::CreateWebview), 50 | ) 51 | } 52 | 53 | fn update(&mut self, message: Message) -> Task { 54 | match message { 55 | Message::WebView(msg) => self.webview.update(msg), 56 | Message::CreateWebview => self 57 | .webview 58 | .update(Action::CreateView(PageType::Url(URL.to_string()))), 59 | Message::WebviewCreated => { 60 | if self.current_view == None { 61 | // if its the first tab change to it, after that require switching manually 62 | return Task::done(Message::CycleWebview); 63 | } 64 | self.num_views += 1; 65 | Task::none() 66 | } 67 | Message::ToggleWebview => { 68 | self.show_webview = !self.show_webview; 69 | Task::none() 70 | } 71 | Message::UrlChanged(new_url) => { 72 | self.webview_url = Some(new_url); 73 | Task::none() 74 | } 75 | Message::CycleWebview => { 76 | if let Some(current_view) = self.current_view.as_mut() { 77 | if *current_view + 1 > self.num_views { 78 | *current_view = 0; 79 | } else { 80 | *current_view += 1; 81 | }; 82 | self.webview.update(Action::ChangeView(*current_view)) 83 | } else { 84 | self.current_view = Some(0); 85 | self.webview.update(Action::ChangeView(0)) 86 | } 87 | } 88 | } 89 | } 90 | 91 | fn view(&self) -> Element { 92 | let mut column = column![row![ 93 | text(if !self.show_webview { 94 | "Click the button to open a webview" 95 | } else { 96 | "Iced docs can be pulled up inside an iced app?! Whoa!" 97 | }), 98 | container(row![ 99 | button("Toggle web view(s)").on_press(Message::ToggleWebview), 100 | button("New web view").on_press(Message::CreateWebview), 101 | button("Switch views").on_press(Message::CycleWebview), 102 | ]) 103 | .align_right(Length::Fill) 104 | ]]; 105 | 106 | if self.show_webview { 107 | if let Some(current_view) = self.current_view { 108 | column = column.push(column![ 109 | text(format!("view index: {}", current_view)), 110 | self.webview.view().map(Message::WebView), 111 | text(format!("Url: {:?}", self.webview_url)), 112 | ]); 113 | } 114 | } 115 | column.into() 116 | } 117 | 118 | fn subscription(&self) -> Subscription { 119 | time::every(Duration::from_millis(10)) 120 | .map(|_| Action::Update) 121 | .map(Message::WebView) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /examples/multi_webview.rs: -------------------------------------------------------------------------------- 1 | use iced::{ 2 | time, 3 | widget::{column, container, row, text}, 4 | Element, Length, Subscription, Task, 5 | }; 6 | use iced_webview::{ 7 | advanced::{Action, WebView}, 8 | PageType, Ultralight, ViewId, 9 | }; 10 | use std::time::Duration; 11 | 12 | static URL1: &'static str = "https://docs.rs/iced/latest/iced/index.html"; 13 | static URL2: &'static str = "https://github.com/LegitCamper/iced_webview"; 14 | 15 | fn main() -> iced::Result { 16 | iced::application("An multi webview application", App::update, App::view) 17 | .subscription(App::subscription) 18 | .run_with(App::new) 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | enum Message { 23 | WebView(Action), 24 | CreatedNewWebView(ViewId), 25 | } 26 | 27 | struct App { 28 | webview: WebView, 29 | webviews: (Option, Option), 30 | } 31 | 32 | impl App { 33 | fn new() -> (Self, Task) { 34 | let webview = WebView::new().on_create_view(Message::CreatedNewWebView); 35 | ( 36 | Self { 37 | webview, 38 | webviews: (None, None), 39 | }, 40 | Task::chain( 41 | Task::done(Action::CreateView(PageType::Url(URL1.to_string()))) 42 | .map(Message::WebView), 43 | Task::done(Action::CreateView(PageType::Url(URL2.to_string()))) 44 | .map(Message::WebView), 45 | ), 46 | ) 47 | } 48 | 49 | fn update(&mut self, message: Message) -> Task { 50 | let mut tasks = Vec::new(); 51 | tasks.push(match message { 52 | Message::WebView(msg) => self.webview.update(msg), 53 | Message::CreatedNewWebView(view_id) => { 54 | if self.webviews.0 == None { 55 | self.webviews.0 = Some(view_id); 56 | } else if self.webviews.1 == None { 57 | self.webviews.1 = Some(view_id); 58 | } 59 | Task::none() 60 | } 61 | }); 62 | if let Some(view_0) = self.webviews.0 { 63 | tasks.push(self.webview.update(Action::Update(view_0))); 64 | } 65 | if let Some(view_1) = self.webviews.1 { 66 | tasks.push(self.webview.update(Action::Update(view_1))); 67 | } 68 | Task::batch(tasks) 69 | } 70 | 71 | fn view(&self) -> Element { 72 | let Some(view1) = self.webviews.0 else { 73 | return text("loading").into(); 74 | }; 75 | let Some(view2) = self.webviews.1 else { 76 | return text("loading").into(); 77 | }; 78 | row![ 79 | container(column![ 80 | text("View 1 of iced docs"), 81 | container(self.webview.view(view1).map(Message::WebView)).height(Length::Fill), 82 | ]) 83 | .padding(5), 84 | container(column![ 85 | text("View 2 of the iced_webview repo"), 86 | container(self.webview.view(view2).map(Message::WebView)).height(Length::Fill), 87 | ]) 88 | .padding(5), 89 | ] 90 | .into() 91 | } 92 | 93 | fn subscription(&self) -> Subscription { 94 | Subscription::batch([time::every(Duration::from_millis(10)) 95 | .map(|_| Action::UpdateAll) 96 | .map(Message::WebView)]) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/engines.rs: -------------------------------------------------------------------------------- 1 | use crate::ImageInfo; 2 | use iced::keyboard; 3 | use iced::mouse::{self, Interaction}; 4 | use iced::Point; 5 | use iced::Size; 6 | 7 | /// A Ultralight implementation of Engine 8 | #[cfg(feature = "ultralight")] 9 | pub mod ultralight; 10 | 11 | /// Creation of new pages to be of a html type or a url 12 | #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 13 | pub enum PageType { 14 | /// Allows visiting Url web pages 15 | Url(String), 16 | /// Allows custom html web pages 17 | Html(String), 18 | } 19 | 20 | /// Enables browser engines to display their images in different formats 21 | pub enum PixelFormat { 22 | /// RGBA 23 | Rgba, 24 | /// BGRA 25 | Bgra, 26 | } 27 | 28 | /// Alias of usize used for controlling specific views 29 | /// Only used by advanced to get views, basic simply uses u32 30 | pub type ViewId = usize; 31 | 32 | /// Trait to handle multiple browser engines 33 | /// Currently only supports cpu renders via pixel_buffer 34 | /// Passing a View id that does not exist will cause a panic 35 | pub trait Engine { 36 | /// Used to do work in the actual browser engine 37 | fn update(&mut self); 38 | /// Has Ultralight perform a new render 39 | fn render(&mut self, size: Size); 40 | /// Request that the browser engine rerender a specific view that may have been updated 41 | fn request_render(&mut self, id: ViewId, size: Size); 42 | /// Creates new a new (possibly blank) view and returns the ViewId to interact with it 43 | fn new_view(&mut self, size: Size, content: Option) -> ViewId; 44 | /// Removes desired view 45 | fn remove_view(&mut self, id: ViewId); 46 | 47 | /// Focuses webview 48 | fn focus(&mut self); 49 | /// Unfocuses webview 50 | fn unfocus(&self); 51 | /// Resizes webview 52 | fn resize(&mut self, size: Size); 53 | 54 | /// lets the engine handle keyboard events 55 | fn handle_keyboard_event(&mut self, id: ViewId, event: keyboard::Event); 56 | /// lets the engine handle mouse events 57 | fn handle_mouse_event(&mut self, id: ViewId, point: Point, event: mouse::Event); 58 | /// Handles Scrolles on view 59 | fn scroll(&mut self, id: ViewId, delta: mouse::ScrollDelta); 60 | 61 | /// Go to a specific page type 62 | fn goto(&mut self, id: ViewId, page_type: PageType); 63 | /// Refresh specific view 64 | fn refresh(&mut self, id: ViewId); 65 | /// Moves forward on view 66 | fn go_forward(&mut self, id: ViewId); 67 | /// Moves back on view 68 | fn go_back(&mut self, id: ViewId); 69 | 70 | /// Gets current url from view 71 | fn get_url(&self, id: ViewId) -> String; 72 | /// Gets current title from view 73 | fn get_title(&self, id: ViewId) -> String; 74 | /// Gets current cursor status from view 75 | fn get_cursor(&self, id: ViewId) -> Interaction; 76 | /// Gets cpu renderered webview 77 | fn get_view(&self, id: ViewId) -> &ImageInfo; 78 | } 79 | -------------------------------------------------------------------------------- /src/engines/ultralight.rs: -------------------------------------------------------------------------------- 1 | use clipboard_rs::{Clipboard, ClipboardContext}; 2 | use iced::keyboard::{self}; 3 | use iced::mouse::{self, ScrollDelta}; 4 | use iced::{Point, Size}; 5 | use rand::Rng; 6 | use smol_str::SmolStr; 7 | use std::path::PathBuf; 8 | use std::str::FromStr; 9 | use std::sync::{Arc, RwLock}; 10 | use std::{env::var, path::Path}; 11 | use ul_next::{ 12 | config::Config, 13 | event::{self, KeyEventCreationInfo, MouseEvent, ScrollEvent}, 14 | key_code::VirtualKeyCode, 15 | platform, 16 | renderer::Renderer, 17 | view, 18 | window::Cursor, 19 | }; 20 | 21 | use super::{Engine, PageType, PixelFormat, ViewId}; 22 | use crate::ImageInfo; 23 | 24 | struct UlClipboard { 25 | ctx: ClipboardContext, 26 | } 27 | 28 | impl platform::Clipboard for UlClipboard { 29 | fn clear(&mut self) {} 30 | 31 | fn read_plain_text(&mut self) -> Option { 32 | Some(self.ctx.get_text().unwrap_or("".to_string())) 33 | } 34 | 35 | fn write_plain_text(&mut self, text: &str) { 36 | self.ctx 37 | .set_text(text.into()) 38 | .expect("Failed to set contents of clipboard"); 39 | } 40 | } 41 | 42 | /// Holds Ultralight View info like surfaces for rendering and urls & titles 43 | pub struct View { 44 | id: ViewId, 45 | view: view::View, 46 | cursor: Arc>, 47 | last_frame: ImageInfo, 48 | was_loading: bool, 49 | cursor_pos: Point, 50 | } 51 | 52 | impl View { 53 | fn update_cursor_pos(&mut self) { 54 | let cursor_pos = self.cursor_pos; 55 | self.view.fire_mouse_event( 56 | MouseEvent::new( 57 | ul_next::event::MouseEventType::MouseMoved, 58 | cursor_pos.x as i32, 59 | cursor_pos.y as i32, 60 | ul_next::event::MouseButton::None, 61 | ) 62 | .expect("Ultralight failed to fire mouse input"), 63 | ); 64 | } 65 | } 66 | 67 | /// Implementation of the Ultralight browsing engine for iced_webivew 68 | pub struct Ultralight { 69 | renderer: Renderer, 70 | view_config: view::ViewConfig, 71 | views: Vec, 72 | } 73 | 74 | impl Default for Ultralight { 75 | fn default() -> Self { 76 | let config = Config::start().build().expect("Failed to start Ultralight"); 77 | platform::enable_platform_fontloader(); 78 | platform::enable_platform_filesystem(platform_filesystem()) 79 | .expect("Failed to get platform filesystem"); 80 | platform::set_clipboard(UlClipboard { 81 | ctx: ClipboardContext::new().expect("Failed to get ownership of clipboard"), 82 | }); 83 | 84 | let renderer = Renderer::create(config).expect("Failed to create ultralight renderer"); 85 | let view_config = view::ViewConfig::start() 86 | .initial_device_scale(1.0) 87 | .font_family_standard("Arial") 88 | .is_accelerated(false) 89 | .build() 90 | .unwrap(); 91 | 92 | Self { 93 | renderer, 94 | view_config, 95 | views: Vec::new(), 96 | } 97 | } 98 | } 99 | 100 | impl Ultralight { 101 | /// Creates a new Ultralight adapter 102 | pub fn new(font: &str, scale: f64) -> Self { 103 | Self { 104 | view_config: view::ViewConfig::start() 105 | .initial_device_scale(scale) 106 | .font_family_standard(font) 107 | // iced_webview does not currently support acceleration 108 | .is_accelerated(false) 109 | .build() 110 | .unwrap(), 111 | ..Default::default() 112 | } 113 | } 114 | 115 | fn get_view(&self, id: ViewId) -> &View { 116 | self.views 117 | .iter() 118 | .find(|&view| view.id == id) 119 | .expect("The requested View id was not found") 120 | } 121 | 122 | fn get_view_mut(&mut self, id: ViewId) -> &mut View { 123 | self.views 124 | .iter_mut() 125 | .find(|view| view.id == id) 126 | .expect("The requested View id was not found") 127 | } 128 | } 129 | 130 | impl Engine for Ultralight { 131 | fn update(&mut self) { 132 | self.renderer.update(); 133 | } 134 | 135 | fn render(&mut self, size: Size) { 136 | // for each view save frame 137 | for view in self.views.iter_mut() { 138 | view.update_cursor_pos(); 139 | if view.view.needs_paint() || view.was_loading && !view.view.is_loading() { 140 | if let Some(pixels) = view.view.surface().unwrap().lock_pixels() { 141 | view.last_frame = 142 | ImageInfo::new(pixels.to_vec(), PixelFormat::Bgra, size.width, size.height); 143 | view.was_loading = false; 144 | } 145 | } 146 | } 147 | } 148 | 149 | fn request_render(&mut self, id: ViewId, size: Size) { 150 | self.get_view_mut(id).update_cursor_pos(); 151 | self.get_view(id).view.set_needs_paint(true); 152 | self.renderer.render(); 153 | if let Some(pixels) = self.get_view(id).view.surface().unwrap().lock_pixels() { 154 | self.get_view_mut(id).last_frame = 155 | ImageInfo::new(pixels.to_vec(), PixelFormat::Bgra, size.width, size.height); 156 | self.get_view_mut(id).was_loading = false 157 | } 158 | } 159 | 160 | fn new_view(&mut self, size: Size, page_type: Option) -> ViewId { 161 | let id = rand::thread_rng().gen(); 162 | 163 | let view = self 164 | .renderer 165 | .create_view(size.width, size.height, &self.view_config, None) 166 | .expect("Failed to create view"); 167 | 168 | // TODO: debug why new views are slanted unless do + 10/ - 10 169 | // maybe causes the fuzzyness 170 | view.resize(size.width + 10, size.height - 10); 171 | 172 | let surface = view.surface().expect("Failed to get surface of new view"); 173 | // RGBA - ensure it has the right diamentions 174 | debug_assert!(surface.row_bytes() / size.width == 4); 175 | 176 | let cursor = Arc::new(RwLock::new(mouse::Interaction::Idle)); 177 | let cb_cursor = cursor.clone(); 178 | view.set_change_cursor_callback(move |_view, cursor_update| { 179 | *cb_cursor.write().expect("Failed to write cursor status") = match cursor_update { 180 | Cursor::None => mouse::Interaction::Idle, 181 | Cursor::Pointer => mouse::Interaction::Idle, 182 | Cursor::Hand => mouse::Interaction::Pointer, 183 | Cursor::Grab => mouse::Interaction::Grab, 184 | Cursor::VerticalText => mouse::Interaction::Text, 185 | Cursor::IBeam => mouse::Interaction::Text, 186 | Cursor::Cross => mouse::Interaction::Crosshair, 187 | Cursor::Wait => mouse::Interaction::Working, 188 | Cursor::Grabbing => mouse::Interaction::Grab, 189 | Cursor::NorthSouthResize => mouse::Interaction::ResizingVertically, 190 | Cursor::EastWestResize => mouse::Interaction::ResizingHorizontally, 191 | Cursor::NotAllowed => mouse::Interaction::NotAllowed, 192 | Cursor::ZoomIn => mouse::Interaction::ZoomIn, 193 | Cursor::ZoomOut => mouse::Interaction::ZoomIn, 194 | _ => mouse::Interaction::Pointer, 195 | }; 196 | }); 197 | 198 | let view = View { 199 | id, 200 | view, 201 | cursor, 202 | last_frame: ImageInfo::blank(size.width, size.height), 203 | was_loading: true, 204 | cursor_pos: Point::default(), 205 | }; 206 | if let Some(page_type) = page_type { 207 | match page_type { 208 | PageType::Url(url) => view.view.load_url(&url).expect("Failed to load url"), 209 | PageType::Html(html) => view 210 | .view 211 | .load_html(&html) 212 | .expect("Failed to load custom html"), 213 | } 214 | view.view.set_needs_paint(true); 215 | } 216 | self.views.push(view); 217 | id 218 | } 219 | 220 | fn remove_view(&mut self, id: ViewId) { 221 | self.views.retain(|view| view.id != id); 222 | } 223 | 224 | fn goto(&mut self, id: ViewId, page_type: PageType) { 225 | *self 226 | .get_view(id) 227 | .cursor 228 | .write() 229 | .expect("Failed cursor poisoned") = mouse::Interaction::Working; 230 | match page_type { 231 | PageType::Url(url) => self 232 | .get_view_mut(id) 233 | .view 234 | .load_url(&url) 235 | .expect("Failed to load url"), 236 | PageType::Html(html) => self 237 | .get_view_mut(id) 238 | .view 239 | .load_html(&html) 240 | .expect("Failed to load given html"), 241 | } 242 | self.get_view_mut(id).was_loading = true; 243 | } 244 | 245 | fn focus(&mut self) { 246 | self.views.iter().for_each(|view| view.view.focus()); 247 | } 248 | 249 | fn unfocus(&self) { 250 | self.views.iter().for_each(|view| view.view.unfocus()); 251 | } 252 | 253 | fn resize(&mut self, size: Size) { 254 | self.views.iter().for_each(|view| { 255 | view.view.resize(size.width, size.height); 256 | view.view.surface().unwrap().resize(size.width, size.height); 257 | view.view.set_needs_paint(true); 258 | }) 259 | } 260 | 261 | fn handle_keyboard_event(&mut self, id: ViewId, event: keyboard::Event) { 262 | let key_event = match event { 263 | keyboard::Event::KeyPressed { 264 | key, 265 | location, 266 | modifiers, 267 | text, 268 | modified_key, 269 | physical_key: _, 270 | } => iced_key_to_ultralight_key( 271 | KeyPress::Press, 272 | Some(modified_key), 273 | Some(key), 274 | Some(location), 275 | modifiers, 276 | text, 277 | ), 278 | keyboard::Event::KeyReleased { 279 | key, 280 | location, 281 | modifiers, 282 | } => iced_key_to_ultralight_key( 283 | KeyPress::Unpress, 284 | None, 285 | Some(key), 286 | Some(location), 287 | modifiers, 288 | None, 289 | ), 290 | keyboard::Event::ModifiersChanged(modifiers) => { 291 | iced_key_to_ultralight_key(KeyPress::Press, None, None, None, modifiers, None) 292 | } 293 | }; 294 | 295 | if let Some(key_event) = key_event { 296 | self.get_view_mut(id).view.fire_key_event(key_event); 297 | } 298 | } 299 | 300 | fn handle_mouse_event(&mut self, id: ViewId, point: Point, event: mouse::Event) { 301 | match event { 302 | mouse::Event::ButtonReleased(mouse::Button::Forward) => self.go_forward(id), 303 | mouse::Event::ButtonReleased(mouse::Button::Back) => self.go_back(id), 304 | mouse::Event::ButtonPressed(mouse::Button::Left) => { 305 | self.get_view_mut(id).view.fire_mouse_event( 306 | MouseEvent::new( 307 | ul_next::event::MouseEventType::MouseDown, 308 | point.x as i32, 309 | point.y as i32, 310 | ul_next::event::MouseButton::Left, 311 | ) 312 | .expect("Ultralight failed to fire mouse input"), 313 | ); 314 | } 315 | mouse::Event::ButtonReleased(mouse::Button::Left) => { 316 | self.get_view_mut(id).view.fire_mouse_event( 317 | MouseEvent::new( 318 | ul_next::event::MouseEventType::MouseUp, 319 | point.x as i32, 320 | point.y as i32, 321 | ul_next::event::MouseButton::Left, 322 | ) 323 | .expect("Ultralight failed to fire mouse input"), 324 | ); 325 | } 326 | mouse::Event::ButtonPressed(mouse::Button::Right) => { 327 | self.get_view_mut(id).view.fire_mouse_event( 328 | MouseEvent::new( 329 | ul_next::event::MouseEventType::MouseDown, 330 | point.x as i32, 331 | point.y as i32, 332 | ul_next::event::MouseButton::Right, 333 | ) 334 | .expect("Ultralight failed to fire mouse input"), 335 | ); 336 | } 337 | mouse::Event::ButtonReleased(mouse::Button::Right) => { 338 | self.get_view_mut(id).view.fire_mouse_event( 339 | MouseEvent::new( 340 | ul_next::event::MouseEventType::MouseUp, 341 | point.x as i32, 342 | point.y as i32, 343 | ul_next::event::MouseButton::Right, 344 | ) 345 | .expect("Ultralight failed to fire mouse input"), 346 | ); 347 | } 348 | mouse::Event::CursorMoved { position: _ } => { 349 | self.get_view_mut(id).cursor_pos = point; 350 | } 351 | mouse::Event::WheelScrolled { delta } => self.scroll(id, delta), 352 | mouse::Event::CursorLeft => { 353 | self.unfocus(); 354 | } 355 | mouse::Event::CursorEntered => { 356 | self.focus(); 357 | } 358 | _ => (), 359 | } 360 | } 361 | 362 | fn refresh(&mut self, id: ViewId) { 363 | self.get_view_mut(id).view.reload(); 364 | } 365 | 366 | fn go_forward(&mut self, id: ViewId) { 367 | self.get_view_mut(id).view.go_forward(); 368 | } 369 | 370 | fn go_back(&mut self, id: ViewId) { 371 | self.get_view_mut(id).view.go_back(); 372 | } 373 | 374 | fn scroll(&mut self, id: ViewId, delta: mouse::ScrollDelta) { 375 | let scroll_event = match delta { 376 | ScrollDelta::Lines { x, y } => ScrollEvent::new( 377 | ul_next::event::ScrollEventType::ScrollByPixel, 378 | x as i32 * 100, 379 | y as i32 * 100, 380 | ) 381 | .unwrap(), 382 | ScrollDelta::Pixels { x, y } => ScrollEvent::new( 383 | ul_next::event::ScrollEventType::ScrollByPixel, 384 | x as i32, 385 | y as i32, 386 | ) 387 | .unwrap(), 388 | }; 389 | self.get_view_mut(id).view.fire_scroll_event(scroll_event); 390 | } 391 | 392 | fn get_url(&self, id: ViewId) -> String { 393 | self.get_view(id).view.url().unwrap_or_default() 394 | } 395 | 396 | fn get_title(&self, id: ViewId) -> String { 397 | self.get_view(id).view.title().unwrap_or_default() 398 | } 399 | 400 | fn get_cursor(&self, id: ViewId) -> mouse::Interaction { 401 | match self.get_view(id).cursor.read() { 402 | Ok(cursor) => *cursor, 403 | Err(_) => mouse::Interaction::Working, 404 | } 405 | } 406 | 407 | fn get_view(&self, id: ViewId) -> &ImageInfo { 408 | &self.get_view(id).last_frame 409 | } 410 | } 411 | 412 | fn platform_filesystem() -> PathBuf { 413 | let env = var("ULTRALIGHT_RESOURCES_DIR"); 414 | let resources_path: PathBuf = match env { 415 | Ok(env) => PathBuf::from_str(&env) 416 | .expect("Failed to get path from ultralight resources enviroment varible"), 417 | Err(_) => { 418 | // env not set - check if its been symlinked by build.rs 419 | match Path::new("./resources").exists() { 420 | true => Path::new("./resources").to_owned(), 421 | false => panic!("ULTRALIGHT_RESOURCES_DIR was not set and ultralight-resources feature was not enabled"), 422 | } 423 | } 424 | }; 425 | assert!(Path::new(&resources_path).join("cacert.pem").exists()); 426 | assert!(Path::new(&resources_path).join("icudt67l.dat").exists()); 427 | resources_path 428 | .parent() // leaves resources directory 429 | .expect("resources path needs to point to the resources directory") 430 | .into() 431 | } 432 | 433 | #[derive(Debug, PartialEq, Eq)] 434 | enum KeyPress { 435 | Press, 436 | Unpress, 437 | } 438 | 439 | fn iced_key_to_ultralight_key( 440 | press: KeyPress, 441 | modified_key: Option, 442 | key: Option, // This one is modified by ctrl and results in wrong key 443 | _location: Option, 444 | modifiers: keyboard::Modifiers, 445 | text: Option, 446 | ) -> Option { 447 | let (text, virtual_key, native_key) = { 448 | if let Some(key) = key { 449 | let text = match key { 450 | keyboard::Key::Named(key) => { 451 | if key == keyboard::key::Named::Space { 452 | String::from(" ") 453 | } else { 454 | String::from("") 455 | } 456 | } 457 | keyboard::Key::Character(_) => match text { 458 | Some(text) => text.to_string(), 459 | None => String::from(""), 460 | }, 461 | keyboard::Key::Unidentified => return None, 462 | }; 463 | let (virtual_key, native_key) = match key { 464 | keyboard::Key::Named(key) => match key { 465 | keyboard::key::Named::Control => ( 466 | VirtualKeyCode::Control, 467 | #[cfg(windows)] 468 | 17, 469 | #[cfg(unix)] 470 | 29, 471 | ), 472 | keyboard::key::Named::Shift => ( 473 | VirtualKeyCode::Shift, 474 | #[cfg(windows)] 475 | 16, 476 | #[cfg(unix)] 477 | 42, 478 | ), 479 | keyboard::key::Named::Enter => ( 480 | VirtualKeyCode::Return, 481 | #[cfg(windows)] 482 | 13, 483 | #[cfg(unix)] 484 | 28, 485 | ), 486 | keyboard::key::Named::Tab => ( 487 | VirtualKeyCode::Tab, 488 | #[cfg(windows)] 489 | 9, 490 | #[cfg(unix)] 491 | 15, 492 | ), 493 | keyboard::key::Named::Space => ( 494 | VirtualKeyCode::Space, 495 | #[cfg(windows)] 496 | 32, 497 | #[cfg(unix)] 498 | 57, 499 | ), 500 | keyboard::key::Named::ArrowDown => ( 501 | VirtualKeyCode::Down, 502 | #[cfg(windows)] 503 | 40, 504 | #[cfg(unix)] 505 | 108, 506 | ), 507 | keyboard::key::Named::ArrowLeft => ( 508 | VirtualKeyCode::Right, 509 | #[cfg(windows)] 510 | 37, 511 | #[cfg(unix)] 512 | 106, 513 | ), 514 | keyboard::key::Named::ArrowRight => ( 515 | VirtualKeyCode::Up, 516 | #[cfg(windows)] 517 | 39, 518 | #[cfg(unix)] 519 | 103, 520 | ), 521 | keyboard::key::Named::ArrowUp => ( 522 | VirtualKeyCode::Left, 523 | #[cfg(windows)] 524 | 33, 525 | #[cfg(unix)] 526 | 105, 527 | ), 528 | keyboard::key::Named::End => ( 529 | VirtualKeyCode::End, 530 | #[cfg(windows)] 531 | 35, 532 | #[cfg(unix)] 533 | 107, 534 | ), 535 | keyboard::key::Named::Home => ( 536 | VirtualKeyCode::Home, 537 | #[cfg(windows)] 538 | 36, 539 | #[cfg(unix)] 540 | 102, 541 | ), 542 | keyboard::key::Named::Backspace => ( 543 | VirtualKeyCode::Back, 544 | #[cfg(windows)] 545 | 8, 546 | #[cfg(unix)] 547 | 14, 548 | ), 549 | keyboard::key::Named::Delete => ( 550 | VirtualKeyCode::Delete, 551 | #[cfg(windows)] 552 | 46, 553 | #[cfg(unix)] 554 | 11, 555 | ), 556 | keyboard::key::Named::Insert => ( 557 | VirtualKeyCode::Insert, 558 | #[cfg(windows)] 559 | 45, 560 | #[cfg(unix)] 561 | 110, 562 | ), 563 | keyboard::key::Named::Escape => ( 564 | VirtualKeyCode::Escape, 565 | #[cfg(windows)] 566 | 27, 567 | #[cfg(unix)] 568 | 1, 569 | ), 570 | keyboard::key::Named::F1 => ( 571 | VirtualKeyCode::F1, 572 | #[cfg(windows)] 573 | 112, 574 | #[cfg(unix)] 575 | 59, 576 | ), 577 | keyboard::key::Named::F2 => ( 578 | VirtualKeyCode::F2, 579 | #[cfg(windows)] 580 | 113, 581 | #[cfg(unix)] 582 | 60, 583 | ), 584 | keyboard::key::Named::F3 => ( 585 | VirtualKeyCode::F3, 586 | #[cfg(windows)] 587 | 114, 588 | #[cfg(unix)] 589 | 61, 590 | ), 591 | keyboard::key::Named::F4 => ( 592 | VirtualKeyCode::F4, 593 | #[cfg(windows)] 594 | 115, 595 | #[cfg(unix)] 596 | 62, 597 | ), 598 | keyboard::key::Named::F5 => ( 599 | VirtualKeyCode::F5, 600 | #[cfg(windows)] 601 | 116, 602 | #[cfg(unix)] 603 | 63, 604 | ), 605 | keyboard::key::Named::F6 => ( 606 | VirtualKeyCode::F6, 607 | #[cfg(windows)] 608 | 117, 609 | #[cfg(unix)] 610 | 64, 611 | ), 612 | keyboard::key::Named::F7 => ( 613 | VirtualKeyCode::F7, 614 | #[cfg(windows)] 615 | 118, 616 | #[cfg(unix)] 617 | 65, 618 | ), 619 | keyboard::key::Named::F8 => ( 620 | VirtualKeyCode::F8, 621 | #[cfg(windows)] 622 | 119, 623 | #[cfg(unix)] 624 | 66, 625 | ), 626 | keyboard::key::Named::F9 => ( 627 | VirtualKeyCode::F9, 628 | #[cfg(windows)] 629 | 120, 630 | #[cfg(unix)] 631 | 67, 632 | ), 633 | keyboard::key::Named::F10 => ( 634 | VirtualKeyCode::F10, 635 | #[cfg(windows)] 636 | 121, 637 | #[cfg(unix)] 638 | 68, 639 | ), 640 | keyboard::key::Named::F11 => ( 641 | VirtualKeyCode::F11, 642 | #[cfg(windows)] 643 | 122, 644 | #[cfg(unix)] 645 | 69, 646 | ), 647 | keyboard::key::Named::F12 => ( 648 | VirtualKeyCode::F12, 649 | #[cfg(windows)] 650 | 123, 651 | #[cfg(unix)] 652 | 70, 653 | ), 654 | _ => return None, 655 | }, 656 | keyboard::Key::Character(key) => match key.as_str() { 657 | "a" => ( 658 | VirtualKeyCode::A, 659 | #[cfg(windows)] 660 | 65, 661 | #[cfg(unix)] 662 | 30, 663 | ), 664 | "b" => ( 665 | VirtualKeyCode::B, 666 | #[cfg(windows)] 667 | 66, 668 | #[cfg(unix)] 669 | 48, 670 | ), 671 | "c" => ( 672 | VirtualKeyCode::C, 673 | #[cfg(windows)] 674 | 67, 675 | #[cfg(unix)] 676 | 46, 677 | ), 678 | "d" => ( 679 | VirtualKeyCode::D, 680 | #[cfg(windows)] 681 | 68, 682 | #[cfg(unix)] 683 | 32, 684 | ), 685 | "e" => ( 686 | VirtualKeyCode::E, 687 | #[cfg(windows)] 688 | 69, 689 | #[cfg(unix)] 690 | 18, 691 | ), 692 | "f" => ( 693 | VirtualKeyCode::F, 694 | #[cfg(windows)] 695 | 70, 696 | #[cfg(unix)] 697 | 33, 698 | ), 699 | "g" => ( 700 | VirtualKeyCode::G, 701 | #[cfg(windows)] 702 | 71, 703 | #[cfg(unix)] 704 | 34, 705 | ), 706 | "h" => ( 707 | VirtualKeyCode::H, 708 | #[cfg(windows)] 709 | 72, 710 | #[cfg(unix)] 711 | 35, 712 | ), 713 | "i" => ( 714 | VirtualKeyCode::I, 715 | #[cfg(windows)] 716 | 73, 717 | #[cfg(unix)] 718 | 23, 719 | ), 720 | "j" => ( 721 | VirtualKeyCode::J, 722 | #[cfg(windows)] 723 | 74, 724 | #[cfg(unix)] 725 | 36, 726 | ), 727 | "k" => ( 728 | VirtualKeyCode::K, 729 | #[cfg(windows)] 730 | 75, 731 | #[cfg(unix)] 732 | 37, 733 | ), 734 | "l" => ( 735 | VirtualKeyCode::L, 736 | #[cfg(windows)] 737 | 76, 738 | #[cfg(unix)] 739 | 38, 740 | ), 741 | "m" => ( 742 | VirtualKeyCode::M, 743 | #[cfg(windows)] 744 | 77, 745 | #[cfg(unix)] 746 | 50, 747 | ), 748 | "n" => ( 749 | VirtualKeyCode::N, 750 | #[cfg(windows)] 751 | 78, 752 | #[cfg(unix)] 753 | 49, 754 | ), 755 | "o" => ( 756 | VirtualKeyCode::O, 757 | #[cfg(windows)] 758 | 79, 759 | #[cfg(unix)] 760 | 24, 761 | ), 762 | "p" => ( 763 | VirtualKeyCode::P, 764 | #[cfg(windows)] 765 | 80, 766 | #[cfg(unix)] 767 | 25, 768 | ), 769 | "q" => ( 770 | VirtualKeyCode::Q, 771 | #[cfg(windows)] 772 | 81, 773 | #[cfg(unix)] 774 | 16, 775 | ), 776 | "r" => ( 777 | VirtualKeyCode::R, 778 | #[cfg(windows)] 779 | 82, 780 | #[cfg(unix)] 781 | 19, 782 | ), 783 | "s" => ( 784 | VirtualKeyCode::S, 785 | #[cfg(windows)] 786 | 83, 787 | #[cfg(unix)] 788 | 31, 789 | ), 790 | "t" => ( 791 | VirtualKeyCode::T, 792 | #[cfg(windows)] 793 | 84, 794 | #[cfg(unix)] 795 | 20, 796 | ), 797 | "u" => ( 798 | VirtualKeyCode::U, 799 | #[cfg(windows)] 800 | 85, 801 | #[cfg(unix)] 802 | 22, 803 | ), 804 | "v" => ( 805 | VirtualKeyCode::V, 806 | #[cfg(windows)] 807 | 86, 808 | #[cfg(unix)] 809 | 47, 810 | ), 811 | "w" => ( 812 | VirtualKeyCode::W, 813 | #[cfg(windows)] 814 | 87, 815 | #[cfg(unix)] 816 | 17, 817 | ), 818 | "x" => ( 819 | VirtualKeyCode::X, 820 | #[cfg(windows)] 821 | 88, 822 | #[cfg(unix)] 823 | 47, 824 | ), 825 | "y" => ( 826 | VirtualKeyCode::Y, 827 | #[cfg(windows)] 828 | 89, 829 | #[cfg(unix)] 830 | 21, 831 | ), 832 | "z" => ( 833 | VirtualKeyCode::Z, 834 | #[cfg(windows)] 835 | 90, 836 | #[cfg(unix)] 837 | 44, 838 | ), 839 | "0" => ( 840 | VirtualKeyCode::Key0, 841 | #[cfg(windows)] 842 | 48, 843 | #[cfg(unix)] 844 | 11, 845 | ), 846 | "1" => ( 847 | VirtualKeyCode::Key1, 848 | #[cfg(windows)] 849 | 49, 850 | #[cfg(unix)] 851 | 2, 852 | ), 853 | "2" => ( 854 | VirtualKeyCode::Key2, 855 | #[cfg(windows)] 856 | 50, 857 | #[cfg(unix)] 858 | 3, 859 | ), 860 | "3" => ( 861 | VirtualKeyCode::Key3, 862 | #[cfg(windows)] 863 | 51, 864 | #[cfg(unix)] 865 | 4, 866 | ), 867 | "4" => ( 868 | VirtualKeyCode::Key4, 869 | #[cfg(windows)] 870 | 52, 871 | #[cfg(unix)] 872 | 5, 873 | ), 874 | "5" => ( 875 | VirtualKeyCode::Key5, 876 | #[cfg(windows)] 877 | 53, 878 | #[cfg(unix)] 879 | 6, 880 | ), 881 | "6" => ( 882 | VirtualKeyCode::Key6, 883 | #[cfg(windows)] 884 | 54, 885 | #[cfg(unix)] 886 | 7, 887 | ), 888 | "7" => ( 889 | VirtualKeyCode::Key7, 890 | #[cfg(windows)] 891 | 55, 892 | #[cfg(unix)] 893 | 8, 894 | ), 895 | "8" => ( 896 | VirtualKeyCode::Key8, 897 | #[cfg(windows)] 898 | 56, 899 | #[cfg(unix)] 900 | 9, 901 | ), 902 | "9" => ( 903 | VirtualKeyCode::Key9, 904 | #[cfg(windows)] 905 | 57, 906 | #[cfg(unix)] 907 | 10, 908 | ), 909 | "," => ( 910 | VirtualKeyCode::OemComma, 911 | #[cfg(windows)] 912 | 188, 913 | #[cfg(unix)] 914 | 51, 915 | ), 916 | "." => ( 917 | VirtualKeyCode::OemPeriod, 918 | #[cfg(windows)] 919 | 190, 920 | #[cfg(unix)] 921 | 52, 922 | ), 923 | ";" => ( 924 | VirtualKeyCode::OemPeriod, 925 | #[cfg(windows)] 926 | 186, 927 | #[cfg(unix)] 928 | 39, 929 | ), 930 | "-" => ( 931 | VirtualKeyCode::OemMinus, 932 | #[cfg(windows)] 933 | 189, 934 | #[cfg(unix)] 935 | 12, 936 | ), 937 | "_" => ( 938 | VirtualKeyCode::OemMinus, 939 | #[cfg(windows)] 940 | 189, 941 | #[cfg(unix)] 942 | 74, 943 | ), 944 | "+" => ( 945 | VirtualKeyCode::OemPlus, 946 | #[cfg(windows)] 947 | 187, 948 | #[cfg(unix)] 949 | 78, 950 | ), 951 | "=" => ( 952 | VirtualKeyCode::OemPlus, 953 | #[cfg(windows)] 954 | 187, 955 | #[cfg(unix)] 956 | 78, 957 | ), 958 | "\\" => ( 959 | VirtualKeyCode::Oem5, 960 | #[cfg(windows)] 961 | 220, 962 | #[cfg(unix)] 963 | 43, 964 | ), 965 | "|" => ( 966 | VirtualKeyCode::Oem5, 967 | #[cfg(windows)] 968 | 220, 969 | #[cfg(unix)] 970 | 43, 971 | ), 972 | "`" => ( 973 | VirtualKeyCode::Oem3, 974 | #[cfg(windows)] 975 | 192, 976 | #[cfg(unix)] 977 | 41, 978 | ), 979 | "?" => ( 980 | VirtualKeyCode::Oem2, 981 | #[cfg(windows)] 982 | 191, 983 | #[cfg(unix)] 984 | 53, 985 | ), 986 | "/" => ( 987 | VirtualKeyCode::Oem2, 988 | #[cfg(windows)] 989 | 191, 990 | #[cfg(unix)] 991 | 53, 992 | ), 993 | ">" => ( 994 | VirtualKeyCode::Oem102, 995 | #[cfg(windows)] 996 | 226, 997 | #[cfg(unix)] 998 | 52, 999 | ), 1000 | "<" => ( 1001 | VirtualKeyCode::Oem102, 1002 | #[cfg(windows)] 1003 | 226, 1004 | #[cfg(unix)] 1005 | 52, 1006 | ), 1007 | "[" => ( 1008 | VirtualKeyCode::Oem4, 1009 | #[cfg(windows)] 1010 | 219, 1011 | #[cfg(unix)] 1012 | 26, 1013 | ), 1014 | "]" => ( 1015 | VirtualKeyCode::Oem6, 1016 | #[cfg(windows)] 1017 | 221, 1018 | #[cfg(unix)] 1019 | 27, 1020 | ), 1021 | _ => return None, 1022 | }, 1023 | keyboard::Key::Unidentified => return None, 1024 | }; 1025 | (text, virtual_key, native_key) 1026 | } else { 1027 | return None; 1028 | } 1029 | }; 1030 | 1031 | let modifiers = event::KeyEventModifiers { 1032 | alt: modifiers.alt(), 1033 | ctrl: modifiers.control(), 1034 | meta: modifiers.logo(), 1035 | shift: modifiers.shift(), 1036 | }; 1037 | 1038 | let ty = if modifiers.ctrl { 1039 | event::KeyEventType::RawKeyDown 1040 | } else if !text.is_empty() && text.is_ascii() && press == KeyPress::Press { 1041 | event::KeyEventType::Char 1042 | } else { 1043 | match press { 1044 | KeyPress::Press => event::KeyEventType::RawKeyDown, 1045 | KeyPress::Unpress => event::KeyEventType::KeyUp, 1046 | } 1047 | }; 1048 | 1049 | let creation_info = KeyEventCreationInfo { 1050 | ty, 1051 | modifiers, 1052 | virtual_key_code: virtual_key, 1053 | native_key_code: native_key, 1054 | text: text.as_str(), 1055 | unmodified_text: if let Some(keyboard::Key::Character(char)) = modified_key { 1056 | &char.to_string() 1057 | } else { 1058 | text.as_str() 1059 | }, 1060 | is_keypad: false, 1061 | is_auto_repeat: false, 1062 | is_system_key: false, 1063 | }; 1064 | 1065 | event::KeyEvent::new(creation_info).ok() 1066 | } 1067 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Iced_webview is a library to embed web views in iced applications. It is a renderer agnostic webview library for Iced. 2 | //! 3 | //! > Note: Currently this library only supports [Ultralight](https://ultralig.ht)/Webkit, but more rendering engines are planned to be supported. 4 | //! > [Ultralight has its own license](https://ultralig.ht/pricing/) that should be reviewed before deciding if it works for you 5 | //! 6 | //! Has two separate widgets: Basic, and Advanced. 7 | //! The basic widget is very simple to implement and requires no knowledge of the widget. 8 | //! You can use simple abstractions like CloseCurrent, and ChangeView. 9 | //! Whereas with the Advanced widget, you have callbacks when a view is done being created, and you need to keep track of the ViewId for view calls 10 | //! 11 | //! # Basic usage should look familiar to iced users: 12 | //! 13 | //! You'll need to create a `Message` for Webview: 14 | //! ```rust 15 | //! enum Message { 16 | //! WebView(iced_webview::Action), 17 | //! Update 18 | //! } 19 | //! ``` 20 | //! 21 | //! Create a new struct to store webview state 22 | //! ```rust 23 | //! struct State { 24 | //! webview: iced_webview::WebView, 25 | //! } 26 | //! # #[derive(Clone)] 27 | //! # enum Message { } 28 | //! ``` 29 | //! 30 | //! ### Then you should be able to call the usual `view/update` methods: 31 | //! 32 | //! ```rust 33 | //! fn update(state: &mut State, message: Message) -> iced::Task { 34 | //! match message { 35 | //! Message::WebView(msg) => state.webview.update(msg), 36 | //! Message::Update => state.webview.update(iced_webview::Action::Update), 37 | //! } 38 | //! } 39 | //! # #[derive(Clone)] 40 | //! # enum Message { WebView(iced_webview::Action), Update } 41 | //! # struct State { webview: iced_webview::WebView } 42 | //! ``` 43 | //! 44 | //! ```rust 45 | //! fn view(state: &mut State, message: Message) -> iced::Element { 46 | //! state.webview.view().map(Message::WebView).into() 47 | //! } 48 | //! # #[derive(Clone)] 49 | //! # enum Message { WebView(iced_webview::Action) } 50 | //! # struct State { webview: iced_webview::WebView } 51 | //! ``` 52 | //! 53 | //! The subscription provides periodic updates so that all the backend rendering is done frequently enough 54 | //! 55 | //! ```rust 56 | //! use iced::time; 57 | //! fn subscription(state: &mut State) -> iced::Subscription { 58 | //! time::every(std::time::Duration::from_millis(10)) 59 | //! .map(|_| iced_webview::Action::Update) 60 | //! .map(Message::WebView) 61 | //! } 62 | //! # #[derive(Clone)] 63 | //! # enum Message { WebView(iced_webview::Action) } 64 | //! # struct State { webview: iced_webview::WebView } 65 | //! ``` 66 | //! 67 | //! 68 | //! Examples can be found in the [iced_webview repo](https://github.com/LegitCamper/iced_webview/tree/main/examples) 69 | //! 70 | use iced::widget::image; 71 | 72 | /// Engine Trait and Engine implementations 73 | pub mod engines; 74 | pub use engines::{Engine, PageType, PixelFormat, ViewId}; 75 | 76 | mod webview; 77 | pub use basic::{Action, WebView}; 78 | pub use webview::{advanced, basic}; // pub these since its the default/reccommended method 79 | 80 | #[cfg(feature = "ultralight")] 81 | pub use engines::ultralight::Ultralight; 82 | 83 | /// Image details for passing the view around 84 | #[derive(Clone, Debug, PartialEq)] 85 | pub struct ImageInfo { 86 | pixels: Vec, 87 | width: u32, 88 | height: u32, 89 | } 90 | 91 | impl Default for ImageInfo { 92 | fn default() -> Self { 93 | Self { 94 | pixels: vec![255; (Self::WIDTH as usize * Self::HEIGHT as usize) * 4], 95 | width: Self::WIDTH, 96 | height: Self::HEIGHT, 97 | } 98 | } 99 | } 100 | 101 | impl ImageInfo { 102 | // The default dimentions 103 | const WIDTH: u32 = 800; 104 | const HEIGHT: u32 = 800; 105 | 106 | fn new(pixels: Vec, format: PixelFormat, width: u32, height: u32) -> Self { 107 | // R, G, B, A 108 | assert_eq!(pixels.len() % 4, 0); 109 | 110 | let pixels = match format { 111 | PixelFormat::Rgba => pixels, 112 | PixelFormat::Bgra => pixels 113 | .chunks(4) 114 | .flat_map(|chunk| [chunk[2], chunk[1], chunk[0], chunk[3]]) 115 | .collect(), 116 | }; 117 | 118 | Self { 119 | pixels, 120 | width, 121 | height, 122 | } 123 | } 124 | 125 | fn as_image(&self) -> image::Image { 126 | image::Image::new(image::Handle::from_rgba( 127 | self.width, 128 | self.height, 129 | self.pixels.clone(), 130 | )) 131 | } 132 | 133 | fn blank(width: u32, height: u32) -> Self { 134 | Self { 135 | pixels: vec![255; (width as usize * height as usize) * 4], 136 | width, 137 | height, 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/webview.rs: -------------------------------------------------------------------------------- 1 | /// Advanced is a more complex interface than basic and assumes the user stores all the view ids themselves. 2 | /// This gives the user more freedom by allowing them to view multiple views at the same time, but removes 3 | /// actions like close current 4 | pub mod advanced; 5 | /// Basic allows users to have simple interfaces like close current and 6 | /// allows users to index views by ints like 0, 1 , or 2 7 | pub mod basic; 8 | -------------------------------------------------------------------------------- /src/webview/advanced.rs: -------------------------------------------------------------------------------- 1 | use iced::advanced::{ 2 | self, 3 | graphics::core::event, 4 | layout, 5 | renderer::{self}, 6 | widget::Tree, 7 | Clipboard, Layout, Shell, Widget, 8 | }; 9 | use iced::event::Status; 10 | use iced::keyboard; 11 | use iced::mouse::{self, Interaction}; 12 | use iced::widget::image::{Handle, Image}; 13 | use iced::{theme::Theme, Event, Length, Rectangle}; 14 | use iced::{Element, Point, Size, Task}; 15 | use url::Url; 16 | 17 | use crate::{engines, ImageInfo, PageType, ViewId}; 18 | 19 | #[allow(missing_docs)] 20 | #[derive(Debug, Clone, PartialEq)] 21 | pub enum Action { 22 | CloseView(ViewId), 23 | CreateView(PageType), 24 | GoBackward(ViewId), 25 | GoForward(ViewId), 26 | GoToUrl(ViewId, Url), 27 | Refresh(ViewId), 28 | SendKeyboardEvent(ViewId, keyboard::Event), 29 | SendMouseEvent(ViewId, mouse::Event, Point), 30 | /// Call this periodically to update a view 31 | Update(ViewId), 32 | /// Call this periodically to update a view(s) 33 | UpdateAll, 34 | Resize(Size), 35 | } 36 | 37 | /// The Advanced WebView widget that creates and shows webview(s) 38 | pub struct WebView 39 | where 40 | Engine: engines::Engine, 41 | { 42 | engine: Engine, 43 | view_size: Size, 44 | on_close_view: Option Message>>, 45 | on_create_view: Option Message>>, 46 | on_url_change: Option Message>>, 47 | urls: Vec<(ViewId, String)>, 48 | on_title_change: Option Message>>, 49 | titles: Vec<(ViewId, String)>, 50 | } 51 | 52 | impl Default 53 | for WebView 54 | { 55 | fn default() -> Self { 56 | WebView { 57 | engine: Engine::default(), 58 | view_size: Size::new(1920, 1080), 59 | on_close_view: None, 60 | on_create_view: None, 61 | on_url_change: None, 62 | urls: Vec::new(), 63 | on_title_change: None, 64 | titles: Vec::new(), 65 | } 66 | } 67 | } 68 | 69 | impl WebView { 70 | /// Create new Advanced Webview widget 71 | pub fn new() -> Self { 72 | Self::default() 73 | } 74 | 75 | /// Subscribe to create view events 76 | pub fn on_create_view(mut self, on_create_view: impl Fn(usize) -> Message + 'static) -> Self { 77 | self.on_create_view = Some(Box::new(on_create_view)); 78 | self 79 | } 80 | 81 | /// Subscribe to close view events 82 | pub fn on_close_view(mut self, on_close_view: impl Fn(usize) -> Message + 'static) -> Self { 83 | self.on_close_view = Some(Box::new(on_close_view)); 84 | self 85 | } 86 | 87 | /// Subscribe to url change events 88 | pub fn on_url_change( 89 | mut self, 90 | on_url_change: impl Fn(ViewId, String) -> Message + 'static, 91 | ) -> Self { 92 | self.on_url_change = Some(Box::new(on_url_change)); 93 | self 94 | } 95 | 96 | /// Subscribe to title change events 97 | pub fn on_title_change( 98 | mut self, 99 | on_title_change: impl Fn(ViewId, String) -> Message + 'static, 100 | ) -> Self { 101 | self.on_title_change = Some(Box::new(on_title_change)); 102 | self 103 | } 104 | 105 | /// Passes update to webview 106 | pub fn update(&mut self, action: Action) -> Task { 107 | let mut tasks = Vec::new(); 108 | 109 | // Check url & title for changes and callback if so 110 | for (id, url) in self.urls.iter_mut() { 111 | if let Some(on_url_change) = &self.on_url_change { 112 | let engine_url = self.engine.get_url(*id); 113 | if *url != engine_url { 114 | *url = engine_url.clone(); 115 | tasks.push(Task::done(on_url_change(*id, engine_url))); 116 | } 117 | } 118 | } 119 | for (id, title) in self.titles.iter_mut() { 120 | if let Some(on_title_change) = &self.on_title_change { 121 | let engine_title = self.engine.get_title(*id); 122 | if *title != engine_title { 123 | *title = engine_title.clone(); 124 | tasks.push(Task::done(on_title_change(*id, engine_title))); 125 | } 126 | } 127 | } 128 | 129 | match action { 130 | Action::CloseView(id) => { 131 | self.engine.remove_view(id); 132 | self.urls.retain(|url| url.0 != id); 133 | self.titles.retain(|title| title.0 != id); 134 | 135 | if let Some(on_view_close) = &self.on_close_view { 136 | tasks.push(Task::done((on_view_close)(id))) 137 | } 138 | } 139 | Action::CreateView(page_type) => { 140 | let id = self.engine.new_view(self.view_size, Some(page_type)); 141 | self.urls.push((id, String::new())); 142 | self.titles.push((id, String::new())); 143 | 144 | if let Some(on_view_create) = &self.on_create_view { 145 | tasks.push(Task::done((on_view_create)(id))) 146 | } 147 | } 148 | Action::GoBackward(id) => { 149 | self.engine.go_back(id); 150 | self.engine.request_render(id, self.view_size); 151 | } 152 | Action::GoForward(id) => { 153 | self.engine.go_forward(id); 154 | self.engine.request_render(id, self.view_size); 155 | } 156 | Action::GoToUrl(id, url) => { 157 | self.engine.goto(id, PageType::Url(url.to_string())); 158 | self.engine.request_render(id, self.view_size); 159 | } 160 | Action::Refresh(id) => { 161 | self.engine.refresh(id); 162 | self.engine.request_render(id, self.view_size); 163 | } 164 | Action::SendKeyboardEvent(id, event) => { 165 | self.engine.handle_keyboard_event(id, event); 166 | self.engine.request_render(id, self.view_size); 167 | } 168 | Action::SendMouseEvent(id, point, event) => { 169 | self.engine.handle_mouse_event(id, event, point); 170 | self.engine.request_render(id, self.view_size); 171 | } 172 | Action::Update(id) => { 173 | self.engine.update(); 174 | self.engine.request_render(id, self.view_size); 175 | } 176 | Action::UpdateAll => { 177 | self.engine.update(); 178 | self.engine.render(self.view_size); 179 | } 180 | Action::Resize(size) => { 181 | self.view_size = size; 182 | self.engine.resize(size); 183 | } 184 | }; 185 | 186 | Task::batch(tasks) 187 | } 188 | 189 | /// Like a normal `view()` method in iced, but takes an id of the desired view 190 | pub fn view(&self, id: usize) -> Element { 191 | WebViewWidget::new( 192 | id, 193 | self.view_size, 194 | self.engine.get_view(id), 195 | self.engine.get_cursor(id), 196 | ) 197 | .into() 198 | } 199 | } 200 | 201 | struct WebViewWidget { 202 | id: ViewId, 203 | bounds: Size, 204 | image: Image, 205 | cursor: Interaction, 206 | } 207 | 208 | impl WebViewWidget { 209 | fn new(id: ViewId, bounds: Size, image: &ImageInfo, cursor: Interaction) -> Self { 210 | Self { 211 | id, 212 | bounds, 213 | image: image.as_image(), 214 | cursor, 215 | } 216 | } 217 | } 218 | 219 | impl Widget for WebViewWidget 220 | where 221 | Renderer: iced::advanced::image::Renderer, 222 | { 223 | fn size(&self) -> Size { 224 | Size { 225 | width: Length::Fill, 226 | height: Length::Fill, 227 | } 228 | } 229 | 230 | fn layout( 231 | &self, 232 | _tree: &mut Tree, 233 | _renderer: &Renderer, 234 | limits: &layout::Limits, 235 | ) -> layout::Node { 236 | layout::Node::new(limits.max()) 237 | } 238 | 239 | fn draw( 240 | &self, 241 | tree: &Tree, 242 | renderer: &mut Renderer, 243 | theme: &Theme, 244 | style: &renderer::Style, 245 | layout: Layout<'_>, 246 | cursor: mouse::Cursor, 247 | viewport: &Rectangle, 248 | ) { 249 | as Widget>::draw( 250 | &self.image, 251 | tree, 252 | renderer, 253 | theme, 254 | style, 255 | layout, 256 | cursor, 257 | viewport, 258 | ) 259 | } 260 | 261 | fn on_event( 262 | &mut self, 263 | _state: &mut Tree, 264 | event: Event, 265 | layout: Layout<'_>, 266 | cursor: mouse::Cursor, 267 | _renderer: &Renderer, 268 | _clipboard: &mut dyn Clipboard, 269 | shell: &mut Shell<'_, Action>, 270 | _viewport: &Rectangle, 271 | ) -> event::Status { 272 | let size = Size::new(layout.bounds().width as u32, layout.bounds().height as u32); 273 | if self.bounds != size { 274 | shell.publish(Action::Resize(size)); 275 | } 276 | 277 | match event { 278 | Event::Keyboard(event) => { 279 | shell.publish(Action::SendKeyboardEvent(self.id, event)); 280 | } 281 | Event::Mouse(event) => { 282 | if let Some(point) = cursor.position_in(layout.bounds()) { 283 | shell.publish(Action::SendMouseEvent(self.id, event, point)); 284 | } 285 | } 286 | _ => (), 287 | } 288 | Status::Ignored 289 | } 290 | 291 | fn mouse_interaction( 292 | &self, 293 | _state: &Tree, 294 | layout: Layout<'_>, 295 | cursor: mouse::Cursor, 296 | _viewport: &Rectangle, 297 | _renderer: &Renderer, 298 | ) -> mouse::Interaction { 299 | if cursor.is_over(layout.bounds()) { 300 | self.cursor 301 | } else { 302 | mouse::Interaction::Idle 303 | } 304 | } 305 | } 306 | 307 | impl<'a, Message: 'a, Renderer> From for Element<'a, Message, Theme, Renderer> 308 | where 309 | Renderer: advanced::Renderer + advanced::image::Renderer, 310 | WebViewWidget: Widget, 311 | { 312 | fn from(widget: WebViewWidget) -> Self { 313 | Self::new(widget) 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/webview/basic.rs: -------------------------------------------------------------------------------- 1 | use iced::advanced::{ 2 | self, 3 | graphics::core::event, 4 | layout, 5 | renderer::{self}, 6 | widget::Tree, 7 | Clipboard, Layout, Shell, Widget, 8 | }; 9 | use iced::event::Status; 10 | use iced::keyboard; 11 | use iced::mouse::{self, Interaction}; 12 | use iced::widget::image::{Handle, Image}; 13 | use iced::{theme::Theme, Event, Length, Rectangle}; 14 | use iced::{Element, Point, Size, Task}; 15 | use url::Url; 16 | 17 | use crate::{engines, ImageInfo, PageType, ViewId}; 18 | 19 | #[allow(missing_docs)] 20 | #[derive(Debug, Clone, PartialEq)] 21 | /// Handles Actions for Basic webview 22 | pub enum Action { 23 | /// Changes view to the desired view index 24 | ChangeView(u32), 25 | /// Closes current window & makes last used view the current one 26 | CloseCurrentView, 27 | /// Closes specific view index 28 | CloseView(u32), 29 | /// Creates a new view and makes its index view + 1 30 | CreateView(PageType), 31 | GoBackward, 32 | GoForward, 33 | GoToUrl(Url), 34 | Refresh, 35 | SendKeyboardEvent(keyboard::Event), 36 | SendMouseEvent(mouse::Event, Point), 37 | /// Allows users to control when the browser engine proccesses interactions in subscriptions 38 | Update, 39 | Resize(Size), 40 | } 41 | 42 | /// The Basic WebView widget that creates and shows webview(s) 43 | pub struct WebView 44 | where 45 | Engine: engines::Engine, 46 | { 47 | engine: Engine, 48 | view_size: Size, 49 | current_view_index: Option, // the index corresponding to the view_ids list of ViewIds 50 | view_ids: Vec, // allow users to index by simple id like 0 or 1 instead of a true id 51 | on_close_view: Option, 52 | on_create_view: Option, 53 | on_url_change: Option Message>>, 54 | url: String, 55 | on_title_change: Option Message>>, 56 | title: String, 57 | } 58 | 59 | impl WebView { 60 | fn get_current_view_id(&self) -> ViewId { 61 | *self 62 | .view_ids 63 | .get(self.current_view_index.expect( 64 | "The current view index is not currently set. Ensure you call the Action prior", 65 | )) 66 | .expect("Could find view index for current view. Maybe its already been closed?") 67 | } 68 | 69 | fn index_as_view_id(&self, index: u32) -> usize { 70 | *self 71 | .view_ids 72 | .get(index as usize) 73 | .expect("Failed to find that index, maybe its already been closed?") 74 | } 75 | } 76 | 77 | impl Default 78 | for WebView 79 | { 80 | fn default() -> Self { 81 | WebView { 82 | engine: Engine::default(), 83 | view_size: Size { 84 | width: 1920, 85 | height: 1080, 86 | }, 87 | current_view_index: None, 88 | view_ids: Vec::new(), 89 | on_close_view: None, 90 | on_create_view: None, 91 | on_url_change: None, 92 | url: String::new(), 93 | on_title_change: None, 94 | title: String::new(), 95 | } 96 | } 97 | } 98 | 99 | impl WebView { 100 | /// Create new basic WebView widget 101 | pub fn new() -> Self { 102 | Self::default() 103 | } 104 | 105 | /// subscribe to create view events 106 | pub fn on_create_view(mut self, on_create_view: Message) -> Self { 107 | self.on_create_view = Some(on_create_view); 108 | self 109 | } 110 | 111 | /// subscribe to close view events 112 | pub fn on_close_view(mut self, on_close_view: Message) -> Self { 113 | self.on_close_view = Some(on_close_view); 114 | self 115 | } 116 | 117 | /// subscribe to url change events 118 | pub fn on_url_change(mut self, on_url_change: impl Fn(String) -> Message + 'static) -> Self { 119 | self.on_url_change = Some(Box::new(on_url_change)); 120 | self 121 | } 122 | 123 | /// subscribe to title change events 124 | pub fn on_title_change( 125 | mut self, 126 | on_title_change: impl Fn(String) -> Message + 'static, 127 | ) -> Self { 128 | self.on_title_change = Some(Box::new(on_title_change)); 129 | self 130 | } 131 | 132 | /// Passes update to webview 133 | pub fn update(&mut self, action: Action) -> Task { 134 | let mut tasks = Vec::new(); 135 | 136 | if self.current_view_index.is_some() { 137 | if let Some(on_url_change) = &self.on_url_change { 138 | let url = self.engine.get_url(self.get_current_view_id()); 139 | if self.url != url { 140 | self.url = url.clone(); 141 | tasks.push(Task::done(on_url_change(url))) 142 | } 143 | } 144 | if let Some(on_title_change) = &self.on_title_change { 145 | let title = self.engine.get_title(self.get_current_view_id()); 146 | if self.title != title { 147 | self.title = title.clone(); 148 | tasks.push(Task::done(on_title_change(title))) 149 | } 150 | } 151 | } 152 | 153 | match action { 154 | Action::ChangeView(index) => { 155 | // TODO: get around new views not rendering?? 156 | { 157 | self.view_size.width += 10; 158 | self.view_size.height -= 10; 159 | self.engine.resize(self.view_size); 160 | self.view_size.width -= 10; 161 | self.view_size.height += 10; 162 | self.engine.resize(self.view_size); 163 | self.engine 164 | .request_render(self.index_as_view_id(index), self.view_size); 165 | } 166 | self.current_view_index = Some(index as usize); 167 | } 168 | Action::CloseCurrentView => { 169 | self.engine.remove_view(self.get_current_view_id()); 170 | self.view_ids.remove(self.current_view_index); 171 | if let Some(on_view_close) = &self.on_close_view { 172 | tasks.push(Task::done(on_view_close.clone())); 173 | } 174 | } 175 | Action::CloseView(index) => { 176 | self.engine.remove_view(self.index_as_view_id(index)); 177 | self.view_ids.remove(index as usize); 178 | 179 | if let Some(on_view_close) = &self.on_close_view { 180 | tasks.push(Task::done(on_view_close.clone())) 181 | } 182 | } 183 | Action::CreateView(page_type) => { 184 | let id = self.engine.new_view(self.view_size, Some(page_type)); 185 | self.view_ids.push(id); 186 | 187 | if let Some(on_view_create) = &self.on_create_view { 188 | tasks.push(Task::done(on_view_create.clone())) 189 | } 190 | } 191 | Action::GoBackward => { 192 | self.engine.go_back(self.get_current_view_id()); 193 | } 194 | Action::GoForward => { 195 | self.engine.go_forward(self.get_current_view_id()); 196 | } 197 | Action::GoToUrl(url) => { 198 | self.engine 199 | .goto(self.get_current_view_id(), PageType::Url(url.to_string())); 200 | } 201 | Action::Refresh => { 202 | self.engine.refresh(self.get_current_view_id()); 203 | } 204 | Action::SendKeyboardEvent(event) => { 205 | self.engine 206 | .handle_keyboard_event(self.get_current_view_id(), event); 207 | } 208 | Action::SendMouseEvent(point, event) => { 209 | self.engine 210 | .handle_mouse_event(self.get_current_view_id(), event, point); 211 | } 212 | Action::Update => { 213 | self.engine.update(); 214 | if self.current_view_index.is_some() { 215 | self.engine 216 | .request_render(self.get_current_view_id(), self.view_size); 217 | } 218 | return Task::batch(tasks); 219 | } 220 | Action::Resize(size) => { 221 | self.view_size = size; 222 | self.engine.resize(size); 223 | } 224 | }; 225 | 226 | if self.current_view_index.is_some() { 227 | self.engine 228 | .request_render(self.get_current_view_id(), self.view_size); 229 | } 230 | 231 | Task::batch(tasks) 232 | } 233 | 234 | /// Returns webview widget for the current view 235 | pub fn view(&self) -> Element { 236 | WebViewWidget::new( 237 | self.engine.get_view(self.get_current_view_id()), 238 | self.engine.get_cursor(self.get_current_view_id()), 239 | ) 240 | .into() 241 | } 242 | } 243 | 244 | struct WebViewWidget<'a> { 245 | image_info: &'a ImageInfo, 246 | cursor: Interaction, 247 | } 248 | 249 | impl<'a> WebViewWidget<'a> { 250 | fn new(image_info: &'a ImageInfo, cursor: Interaction) -> Self { 251 | Self { image_info, cursor } 252 | } 253 | } 254 | 255 | impl Widget for WebViewWidget<'_> 256 | where 257 | Renderer: iced::advanced::image::Renderer, 258 | { 259 | fn size(&self) -> Size { 260 | Size { 261 | width: Length::Fill, 262 | height: Length::Fill, 263 | } 264 | } 265 | 266 | fn layout( 267 | &self, 268 | _tree: &mut Tree, 269 | _renderer: &Renderer, 270 | limits: &layout::Limits, 271 | ) -> layout::Node { 272 | layout::Node::new(limits.max()) 273 | } 274 | 275 | fn draw( 276 | &self, 277 | tree: &Tree, 278 | renderer: &mut Renderer, 279 | theme: &Theme, 280 | style: &renderer::Style, 281 | layout: Layout<'_>, 282 | cursor: mouse::Cursor, 283 | viewport: &Rectangle, 284 | ) { 285 | as Widget>::draw( 286 | &self.image_info.as_image(), 287 | tree, 288 | renderer, 289 | theme, 290 | style, 291 | layout, 292 | cursor, 293 | viewport, 294 | ) 295 | } 296 | 297 | fn on_event( 298 | &mut self, 299 | _state: &mut Tree, 300 | event: Event, 301 | layout: Layout<'_>, 302 | cursor: mouse::Cursor, 303 | _renderer: &Renderer, 304 | _clipboard: &mut dyn Clipboard, 305 | shell: &mut Shell<'_, Action>, 306 | _viewport: &Rectangle, 307 | ) -> event::Status { 308 | let size = Size::new(layout.bounds().width as u32, layout.bounds().height as u32); 309 | if self.image_info.width != size.width || self.image_info.height != size.height { 310 | shell.publish(Action::Resize(size)); 311 | } 312 | 313 | match event { 314 | Event::Keyboard(event) => { 315 | shell.publish(Action::SendKeyboardEvent(event)); 316 | } 317 | Event::Mouse(event) => { 318 | if let Some(point) = cursor.position_in(layout.bounds()) { 319 | shell.publish(Action::SendMouseEvent(event, point)); 320 | } 321 | } 322 | _ => (), 323 | } 324 | Status::Ignored 325 | } 326 | 327 | fn mouse_interaction( 328 | &self, 329 | _state: &Tree, 330 | layout: Layout<'_>, 331 | cursor: mouse::Cursor, 332 | _viewport: &Rectangle, 333 | _renderer: &Renderer, 334 | ) -> mouse::Interaction { 335 | if cursor.is_over(layout.bounds()) { 336 | self.cursor 337 | } else { 338 | mouse::Interaction::Idle 339 | } 340 | } 341 | } 342 | 343 | impl<'a, Message: 'a, Renderer> From> for Element<'a, Message, Theme, Renderer> 344 | where 345 | Renderer: advanced::Renderer + advanced::image::Renderer, 346 | WebViewWidget<'a>: Widget, 347 | { 348 | fn from(widget: WebViewWidget<'a>) -> Self { 349 | Self::new(widget) 350 | } 351 | } 352 | --------------------------------------------------------------------------------