├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── clippy.toml ├── examples ├── open_parented.rs ├── open_window.rs └── render_femtovg │ ├── Cargo.toml │ └── src │ └── main.rs └── src ├── clipboard.rs ├── event.rs ├── gl ├── macos.rs ├── mod.rs ├── win.rs ├── x11.rs └── x11 │ └── errors.rs ├── keyboard.rs ├── lib.rs ├── macos ├── keyboard.rs ├── mod.rs ├── view.rs └── window.rs ├── mouse_cursor.rs ├── win ├── cursor.rs ├── drop_target.rs ├── keyboard.rs ├── mod.rs └── window.rs ├── window.rs ├── window_info.rs ├── window_open_options.rs └── x11 ├── cursor.rs ├── event_loop.rs ├── keyboard.rs ├── mod.rs ├── visual_info.rs ├── window.rs └── xcb_connection.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | env: 5 | CARGO_TERM_COLOR: always 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | runs-on: ${{ matrix.os }} 13 | env: 14 | RUSTFLAGS: -D warnings 15 | RUSTDOCFLAGS: -D warnings 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Install XCB and GL dependencies 19 | if: contains(matrix.os, 'ubuntu') 20 | run: sudo apt-get install libx11-dev libxcb1-dev libx11-xcb-dev libgl1-mesa-dev 21 | - name: Install rust stable 22 | uses: dtolnay/rust-toolchain@stable 23 | with: 24 | toolchain: stable 25 | components: rustfmt, clippy 26 | - name: Build Default 27 | run: cargo build --workspace --all-targets --verbose 28 | - name: Build All Features 29 | run: cargo build --workspace --all-targets --all-features --verbose 30 | - name: Run tests 31 | run: cargo test --workspace --all-targets --all-features --verbose 32 | - name: Check docs 33 | run: cargo doc --examples --all-features --no-deps 34 | - name: Check Formatting (rustfmt) 35 | run: cargo fmt --all -- --check 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .idea 4 | *.iml 5 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | fn_params_layout = "Compressed" 2 | use_small_heuristics = "Max" 3 | use_field_init_shorthand = true 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "baseview" 3 | version = "0.1.0" 4 | authors = [ 5 | "William Light ", 6 | "Charles Saracco ", 7 | "Mirko Covizzi ", 8 | "Micah Johnston ", 9 | "Billy Messenger ", 10 | "Anton Lazarev ", 11 | "Joakim Frostegård ", 12 | "Robbert van der Helm ", 13 | ] 14 | edition = "2018" 15 | license = "MIT OR Apache-2.0" 16 | 17 | [features] 18 | default = [] 19 | opengl = ["uuid", "x11/glx"] 20 | 21 | [dependencies] 22 | keyboard-types = { version = "0.6.1", default-features = false } 23 | raw-window-handle = "0.5" 24 | 25 | [target.'cfg(target_os="linux")'.dependencies] 26 | x11rb = { version = "0.13.0", features = ["cursor", "resource_manager", "allow-unsafe-code"] } 27 | x11 = { version = "2.21", features = ["xlib", "xlib_xcb"] } 28 | nix = "0.22.0" 29 | 30 | [target.'cfg(target_os="windows")'.dependencies] 31 | winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", "minwindef", "guiddef", "combaseapi", "wingdi", "errhandlingapi", "ole2", "oleidl", "shellapi", "winerror"] } 32 | uuid = { version = "0.8", features = ["v4"], optional = true } 33 | 34 | [target.'cfg(target_os="macos")'.dependencies] 35 | cocoa = "0.24.0" 36 | core-foundation = "0.9.1" 37 | objc = "0.2.7" 38 | uuid = { version = "0.8", features = ["v4"] } 39 | 40 | [dev-dependencies] 41 | rtrb = "0.2" 42 | softbuffer = "0.3.4" 43 | 44 | [workspace] 45 | members = ["examples/render_femtovg"] 46 | 47 | [lints.clippy] 48 | missing-safety-doc = "allow" 49 | 50 | [[example]] 51 | name = "open_window" 52 | test = true 53 | doctest = true 54 | 55 | [[example]] 56 | name = "open_parented" 57 | test = true 58 | doctest = true 59 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # baseview 2 | 3 | A low-level windowing system geared towards making audio plugin UIs. 4 | 5 | `baseview` abstracts the platform-specific windowing APIs (winapi, cocoa, xcb) into a platform-independent API, but otherwise gets out of your way so you can write plugin UIs. 6 | 7 | Interested in learning more about the project? Join us on [discord](https://discord.gg/b3hjnGw), channel `#plugin-gui`. 8 | 9 | ## Roadmap 10 | 11 | Below is a proposed list of milestones (roughly in-order) and their status. Subject to change at any time. 12 | 13 | | Feature | Windows | Mac OS | Linux | 14 | | ----------------------------------------------------- | ------------------ | ------------------ | ------------------ | 15 | | Spawns a window, no parent | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 16 | | Cross-platform API for window spawning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 17 | | Can find DPI scale factor | | :heavy_check_mark: | :heavy_check_mark: | 18 | | Basic event handling (mouse, keyboard) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 19 | | Parent window support | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 20 | | OpenGL context creation (behind the `opengl` feature) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 21 | 22 | ## Prerequisites 23 | 24 | ### Linux 25 | 26 | Install dependencies, e.g.: 27 | 28 | ```sh 29 | sudo apt-get install libx11-dev libxcb1-dev libx11-xcb-dev libgl1-mesa-dev 30 | ``` 31 | 32 | ## License 33 | 34 | Licensed under either of Apache License, Version 35 | 2.0 or MIT license at your option. 36 | 37 | Unless you explicitly state otherwise, any contribution intentionally submitted 38 | for inclusion in Baseview by you, as defined in the Apache-2.0 license, shall be 39 | dual licensed as above, without any additional terms or conditions. 40 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = '1.59' 2 | check-private-items = true 3 | -------------------------------------------------------------------------------- /examples/open_parented.rs: -------------------------------------------------------------------------------- 1 | use baseview::{ 2 | Event, EventStatus, PhySize, Window, WindowEvent, WindowHandle, WindowHandler, 3 | WindowScalePolicy, 4 | }; 5 | use std::num::NonZeroU32; 6 | 7 | struct ParentWindowHandler { 8 | _ctx: softbuffer::Context, 9 | surface: softbuffer::Surface, 10 | current_size: PhySize, 11 | damaged: bool, 12 | 13 | _child_window: Option, 14 | } 15 | 16 | impl ParentWindowHandler { 17 | pub fn new(window: &mut Window) -> Self { 18 | let ctx = unsafe { softbuffer::Context::new(window) }.unwrap(); 19 | let mut surface = unsafe { softbuffer::Surface::new(&ctx, window) }.unwrap(); 20 | surface.resize(NonZeroU32::new(512).unwrap(), NonZeroU32::new(512).unwrap()).unwrap(); 21 | 22 | let window_open_options = baseview::WindowOpenOptions { 23 | title: "baseview child".into(), 24 | size: baseview::Size::new(256.0, 256.0), 25 | scale: WindowScalePolicy::SystemScaleFactor, 26 | 27 | // TODO: Add an example that uses the OpenGL context 28 | #[cfg(feature = "opengl")] 29 | gl_config: None, 30 | }; 31 | let child_window = 32 | Window::open_parented(window, window_open_options, ChildWindowHandler::new); 33 | 34 | // TODO: no way to query physical size initially? 35 | Self { 36 | _ctx: ctx, 37 | surface, 38 | current_size: PhySize::new(512, 512), 39 | damaged: true, 40 | _child_window: Some(child_window), 41 | } 42 | } 43 | } 44 | 45 | impl WindowHandler for ParentWindowHandler { 46 | fn on_frame(&mut self, _window: &mut Window) { 47 | let mut buf = self.surface.buffer_mut().unwrap(); 48 | if self.damaged { 49 | buf.fill(0xFFAAAAAA); 50 | self.damaged = false; 51 | } 52 | buf.present().unwrap(); 53 | } 54 | 55 | fn on_event(&mut self, _window: &mut Window, event: Event) -> EventStatus { 56 | match event { 57 | Event::Window(WindowEvent::Resized(info)) => { 58 | println!("Parent Resized: {:?}", info); 59 | let new_size = info.physical_size(); 60 | self.current_size = new_size; 61 | 62 | if let (Some(width), Some(height)) = 63 | (NonZeroU32::new(new_size.width), NonZeroU32::new(new_size.height)) 64 | { 65 | self.surface.resize(width, height).unwrap(); 66 | self.damaged = true; 67 | } 68 | } 69 | Event::Mouse(e) => println!("Parent Mouse event: {:?}", e), 70 | Event::Keyboard(e) => println!("Parent Keyboard event: {:?}", e), 71 | Event::Window(e) => println!("Parent Window event: {:?}", e), 72 | } 73 | 74 | EventStatus::Captured 75 | } 76 | } 77 | 78 | struct ChildWindowHandler { 79 | _ctx: softbuffer::Context, 80 | surface: softbuffer::Surface, 81 | current_size: PhySize, 82 | damaged: bool, 83 | } 84 | 85 | impl ChildWindowHandler { 86 | pub fn new(window: &mut Window) -> Self { 87 | let ctx = unsafe { softbuffer::Context::new(window) }.unwrap(); 88 | let mut surface = unsafe { softbuffer::Surface::new(&ctx, window) }.unwrap(); 89 | surface.resize(NonZeroU32::new(512).unwrap(), NonZeroU32::new(512).unwrap()).unwrap(); 90 | 91 | // TODO: no way to query physical size initially? 92 | Self { _ctx: ctx, surface, current_size: PhySize::new(256, 256), damaged: true } 93 | } 94 | } 95 | 96 | impl WindowHandler for ChildWindowHandler { 97 | fn on_frame(&mut self, _window: &mut Window) { 98 | let mut buf = self.surface.buffer_mut().unwrap(); 99 | if self.damaged { 100 | buf.fill(0xFFAA0000); 101 | self.damaged = false; 102 | } 103 | buf.present().unwrap(); 104 | } 105 | 106 | fn on_event(&mut self, _window: &mut Window, event: Event) -> EventStatus { 107 | match event { 108 | Event::Window(WindowEvent::Resized(info)) => { 109 | println!("Child Resized: {:?}", info); 110 | let new_size = info.physical_size(); 111 | self.current_size = new_size; 112 | 113 | if let (Some(width), Some(height)) = 114 | (NonZeroU32::new(new_size.width), NonZeroU32::new(new_size.height)) 115 | { 116 | self.surface.resize(width, height).unwrap(); 117 | self.damaged = true; 118 | } 119 | } 120 | Event::Mouse(e) => println!("Child Mouse event: {:?}", e), 121 | Event::Keyboard(e) => println!("Child Keyboard event: {:?}", e), 122 | Event::Window(e) => println!("Child Window event: {:?}", e), 123 | } 124 | 125 | EventStatus::Captured 126 | } 127 | } 128 | 129 | fn main() { 130 | let window_open_options = baseview::WindowOpenOptions { 131 | title: "baseview".into(), 132 | size: baseview::Size::new(512.0, 512.0), 133 | scale: WindowScalePolicy::SystemScaleFactor, 134 | 135 | // TODO: Add an example that uses the OpenGL context 136 | #[cfg(feature = "opengl")] 137 | gl_config: None, 138 | }; 139 | 140 | Window::open_blocking(window_open_options, ParentWindowHandler::new); 141 | } 142 | -------------------------------------------------------------------------------- /examples/open_window.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroU32; 2 | use std::time::Duration; 3 | 4 | use rtrb::{Consumer, RingBuffer}; 5 | 6 | #[cfg(target_os = "macos")] 7 | use baseview::{copy_to_clipboard, MouseEvent}; 8 | use baseview::{ 9 | Event, EventStatus, PhySize, Window, WindowEvent, WindowHandler, WindowScalePolicy, 10 | }; 11 | 12 | #[derive(Debug, Clone)] 13 | enum Message { 14 | Hello, 15 | } 16 | 17 | struct OpenWindowExample { 18 | rx: Consumer, 19 | 20 | _ctx: softbuffer::Context, 21 | surface: softbuffer::Surface, 22 | current_size: PhySize, 23 | damaged: bool, 24 | } 25 | 26 | impl WindowHandler for OpenWindowExample { 27 | fn on_frame(&mut self, _window: &mut Window) { 28 | let mut buf = self.surface.buffer_mut().unwrap(); 29 | if self.damaged { 30 | buf.fill(0xFFAAAAAA); 31 | self.damaged = false; 32 | } 33 | buf.present().unwrap(); 34 | 35 | while let Ok(message) = self.rx.pop() { 36 | println!("Message: {:?}", message); 37 | } 38 | } 39 | 40 | fn on_event(&mut self, _window: &mut Window, event: Event) -> EventStatus { 41 | match &event { 42 | #[cfg(target_os = "macos")] 43 | Event::Mouse(MouseEvent::ButtonPressed { .. }) => copy_to_clipboard("This is a test!"), 44 | Event::Window(WindowEvent::Resized(info)) => { 45 | println!("Resized: {:?}", info); 46 | let new_size = info.physical_size(); 47 | self.current_size = new_size; 48 | 49 | if let (Some(width), Some(height)) = 50 | (NonZeroU32::new(new_size.width), NonZeroU32::new(new_size.height)) 51 | { 52 | self.surface.resize(width, height).unwrap(); 53 | self.damaged = true; 54 | } 55 | } 56 | _ => {} 57 | } 58 | 59 | log_event(&event); 60 | 61 | EventStatus::Captured 62 | } 63 | } 64 | 65 | fn main() { 66 | let window_open_options = baseview::WindowOpenOptions { 67 | title: "baseview".into(), 68 | size: baseview::Size::new(512.0, 512.0), 69 | scale: WindowScalePolicy::SystemScaleFactor, 70 | 71 | // TODO: Add an example that uses the OpenGL context 72 | #[cfg(feature = "opengl")] 73 | gl_config: None, 74 | }; 75 | 76 | let (mut tx, rx) = RingBuffer::new(128); 77 | 78 | std::thread::spawn(move || loop { 79 | std::thread::sleep(Duration::from_secs(5)); 80 | 81 | if tx.push(Message::Hello).is_err() { 82 | println!("Failed sending message"); 83 | } 84 | }); 85 | 86 | Window::open_blocking(window_open_options, |window| { 87 | let ctx = unsafe { softbuffer::Context::new(window) }.unwrap(); 88 | let mut surface = unsafe { softbuffer::Surface::new(&ctx, window) }.unwrap(); 89 | surface.resize(NonZeroU32::new(512).unwrap(), NonZeroU32::new(512).unwrap()).unwrap(); 90 | 91 | OpenWindowExample { 92 | _ctx: ctx, 93 | surface, 94 | rx, 95 | current_size: PhySize::new(512, 512), 96 | damaged: true, 97 | } 98 | }); 99 | } 100 | 101 | fn log_event(event: &Event) { 102 | match event { 103 | Event::Mouse(e) => println!("Mouse event: {:?}", e), 104 | Event::Keyboard(e) => println!("Keyboard event: {:?}", e), 105 | Event::Window(e) => println!("Window event: {:?}", e), 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /examples/render_femtovg/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "render_femtovg" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | baseview = { path = "../..", features = ["opengl"] } 9 | femtovg = "0.9.0" -------------------------------------------------------------------------------- /examples/render_femtovg/src/main.rs: -------------------------------------------------------------------------------- 1 | use baseview::gl::GlConfig; 2 | use baseview::{ 3 | Event, EventStatus, MouseEvent, PhyPoint, Size, Window, WindowEvent, WindowHandler, WindowInfo, 4 | WindowOpenOptions, WindowScalePolicy, 5 | }; 6 | use femtovg::renderer::OpenGl; 7 | use femtovg::{Canvas, Color}; 8 | 9 | struct FemtovgExample { 10 | canvas: Canvas, 11 | current_size: WindowInfo, 12 | current_mouse_position: PhyPoint, 13 | damaged: bool, 14 | } 15 | 16 | impl FemtovgExample { 17 | fn new(window: &mut Window) -> Self { 18 | let context = window.gl_context().unwrap(); 19 | unsafe { context.make_current() }; 20 | 21 | let renderer = 22 | unsafe { OpenGl::new_from_function(|s| context.get_proc_address(s)) }.unwrap(); 23 | 24 | let mut canvas = Canvas::new(renderer).unwrap(); 25 | // TODO: get actual window width 26 | canvas.set_size(512, 512, 1.0); 27 | 28 | unsafe { context.make_not_current() }; 29 | Self { 30 | canvas, 31 | current_size: WindowInfo::from_logical_size(Size { width: 512.0, height: 512.0 }, 1.0), 32 | current_mouse_position: PhyPoint { x: 256, y: 256 }, 33 | damaged: true, 34 | } 35 | } 36 | } 37 | 38 | impl WindowHandler for FemtovgExample { 39 | fn on_frame(&mut self, window: &mut Window) { 40 | if !self.damaged { 41 | return; 42 | } 43 | 44 | let context = window.gl_context().unwrap(); 45 | unsafe { context.make_current() }; 46 | 47 | let screen_height = self.canvas.height(); 48 | let screen_width = self.canvas.width(); 49 | 50 | // Clear 51 | self.canvas.clear_rect(0, 0, screen_width, screen_height, Color::rgb(0xAA, 0xAA, 0xAA)); 52 | 53 | // Make big blue rectangle 54 | self.canvas.clear_rect( 55 | (screen_width as f32 * 0.1).floor() as u32, 56 | (screen_height as f32 * 0.1).floor() as u32, 57 | (screen_width as f32 * 0.8).floor() as u32, 58 | (screen_height as f32 * 0.8).floor() as u32, 59 | Color::rgbf(0., 0.3, 0.9), 60 | ); 61 | 62 | // Make smol orange rectangle 63 | self.canvas.clear_rect( 64 | (self.current_mouse_position.x - 15).clamp(0, screen_width as i32 - 30) as u32, 65 | (self.current_mouse_position.y - 15).clamp(0, screen_height as i32 - 30) as u32, 66 | 30, 67 | 30, 68 | Color::rgbf(0.9, 0.3, 0.), 69 | ); 70 | 71 | // Tell renderer to execute all drawing commands 72 | self.canvas.flush(); 73 | context.swap_buffers(); 74 | unsafe { context.make_not_current() }; 75 | self.damaged = false; 76 | } 77 | 78 | fn on_event(&mut self, _window: &mut Window, event: Event) -> EventStatus { 79 | match event { 80 | Event::Window(WindowEvent::Resized(size)) => { 81 | let phy_size = size.physical_size(); 82 | self.current_size = size; 83 | self.canvas.set_size(phy_size.width, phy_size.height, size.scale() as f32); 84 | self.damaged = true; 85 | } 86 | Event::Mouse(MouseEvent::CursorMoved { position, .. }) => { 87 | self.current_mouse_position = position.to_physical(&self.current_size); 88 | self.damaged = true; 89 | } 90 | _ => {} 91 | }; 92 | log_event(&event); 93 | EventStatus::Captured 94 | } 95 | } 96 | 97 | fn main() { 98 | let window_open_options = WindowOpenOptions { 99 | title: "Femtovg on Baseview".into(), 100 | size: Size::new(512.0, 512.0), 101 | scale: WindowScalePolicy::SystemScaleFactor, 102 | 103 | gl_config: Some(GlConfig { alpha_bits: 8, ..GlConfig::default() }), 104 | }; 105 | 106 | Window::open_blocking(window_open_options, FemtovgExample::new); 107 | } 108 | 109 | fn log_event(event: &Event) { 110 | match event { 111 | Event::Mouse(e) => println!("Mouse event: {:?}", e), 112 | Event::Keyboard(e) => println!("Keyboard event: {:?}", e), 113 | Event::Window(e) => println!("Window event: {:?}", e), 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/clipboard.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | use crate::macos as platform; 3 | #[cfg(target_os = "windows")] 4 | use crate::win as platform; 5 | #[cfg(target_os = "linux")] 6 | use crate::x11 as platform; 7 | 8 | pub fn copy_to_clipboard(data: &str) { 9 | platform::copy_to_clipboard(data) 10 | } 11 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use keyboard_types::{KeyboardEvent, Modifiers}; 4 | 5 | use crate::{Point, WindowInfo}; 6 | 7 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 8 | pub enum MouseButton { 9 | Left, 10 | Middle, 11 | Right, 12 | Back, 13 | Forward, 14 | Other(u8), 15 | } 16 | 17 | /// A scroll movement. 18 | #[derive(Debug, Clone, Copy, PartialEq)] 19 | pub enum ScrollDelta { 20 | /// A line-based scroll movement 21 | Lines { 22 | /// The number of horizontal lines scrolled 23 | x: f32, 24 | 25 | /// The number of vertical lines scrolled 26 | y: f32, 27 | }, 28 | /// A pixel-based scroll movement 29 | Pixels { 30 | /// The number of horizontal pixels scrolled 31 | x: f32, 32 | /// The number of vertical pixels scrolled 33 | y: f32, 34 | }, 35 | } 36 | 37 | #[derive(Debug, Clone, PartialEq)] 38 | pub enum MouseEvent { 39 | /// The mouse cursor was moved 40 | CursorMoved { 41 | /// The logical coordinates of the mouse position 42 | position: Point, 43 | /// The modifiers that were held down just before the event. 44 | modifiers: Modifiers, 45 | }, 46 | 47 | /// A mouse button was pressed. 48 | ButtonPressed { 49 | /// The button that was pressed. 50 | button: MouseButton, 51 | /// The modifiers that were held down just before the event. 52 | modifiers: Modifiers, 53 | }, 54 | 55 | /// A mouse button was released. 56 | ButtonReleased { 57 | /// The button that was released. 58 | button: MouseButton, 59 | /// The modifiers that were held down just before the event. 60 | modifiers: Modifiers, 61 | }, 62 | 63 | /// The mouse wheel was scrolled. 64 | WheelScrolled { 65 | /// How much was scrolled, in factional lines. 66 | delta: ScrollDelta, 67 | /// The modifiers that were held down just before the event. 68 | modifiers: Modifiers, 69 | }, 70 | 71 | /// The mouse cursor entered the window. 72 | /// 73 | /// May not be available on all platforms. 74 | CursorEntered, 75 | 76 | /// The mouse cursor left the window. 77 | /// 78 | /// May not be available on all platforms. 79 | CursorLeft, 80 | 81 | DragEntered { 82 | /// The logical coordinates of the mouse position 83 | position: Point, 84 | /// The modifiers that were held down just before the event. 85 | modifiers: Modifiers, 86 | /// Data being dragged 87 | data: DropData, 88 | }, 89 | 90 | DragMoved { 91 | /// The logical coordinates of the mouse position 92 | position: Point, 93 | /// The modifiers that were held down just before the event. 94 | modifiers: Modifiers, 95 | /// Data being dragged 96 | data: DropData, 97 | }, 98 | 99 | DragLeft, 100 | 101 | DragDropped { 102 | /// The logical coordinates of the mouse position 103 | position: Point, 104 | /// The modifiers that were held down just before the event. 105 | modifiers: Modifiers, 106 | /// Data being dragged 107 | data: DropData, 108 | }, 109 | } 110 | 111 | #[derive(Debug, Clone)] 112 | pub enum WindowEvent { 113 | Resized(WindowInfo), 114 | Focused, 115 | Unfocused, 116 | WillClose, 117 | } 118 | 119 | #[derive(Debug, Clone)] 120 | pub enum Event { 121 | Mouse(MouseEvent), 122 | Keyboard(KeyboardEvent), 123 | Window(WindowEvent), 124 | } 125 | 126 | #[derive(Debug, Clone, Copy, PartialEq)] 127 | pub enum DropEffect { 128 | Copy, 129 | Move, 130 | Link, 131 | Scroll, 132 | } 133 | 134 | #[derive(Debug, Clone, PartialEq)] 135 | pub enum DropData { 136 | None, 137 | Files(Vec), 138 | } 139 | 140 | /// Return value for [WindowHandler::on_event](`crate::WindowHandler::on_event()`), 141 | /// indicating whether the event was handled by your window or should be passed 142 | /// back to the platform. 143 | /// 144 | /// For most event types, this value won't have any effect. This is the case 145 | /// when there is no clear meaning of passing back the event to the platform, 146 | /// or it isn't obviously useful. Currently, only [`Event::Keyboard`] variants 147 | /// are supported. 148 | #[derive(Debug, Clone, Copy, PartialEq)] 149 | pub enum EventStatus { 150 | /// Event was handled by your window and will not be sent back to the 151 | /// platform for further processing. 152 | Captured, 153 | /// Event was **not** handled by your window, so pass it back to the 154 | /// platform. For parented windows, this usually means that the parent 155 | /// window will receive the event. This is useful for cases such as using 156 | /// DAW functionality for playing piano keys with the keyboard while a 157 | /// plugin window is in focus. 158 | Ignored, 159 | /// We are prepared to handle the data in the drag and dropping will 160 | /// result in [DropEffect] 161 | AcceptDrop(DropEffect), 162 | } 163 | -------------------------------------------------------------------------------- /src/gl/macos.rs: -------------------------------------------------------------------------------- 1 | // This is required because the objc crate is causing a lot of warnings: https://github.com/SSheldon/rust-objc/issues/125 2 | // Eventually we should migrate to the objc2 crate and remove this. 3 | #![allow(unexpected_cfgs)] 4 | 5 | use std::ffi::c_void; 6 | use std::str::FromStr; 7 | 8 | use raw_window_handle::RawWindowHandle; 9 | 10 | use cocoa::appkit::{ 11 | NSOpenGLContext, NSOpenGLContextParameter, NSOpenGLPFAAccelerated, NSOpenGLPFAAlphaSize, 12 | NSOpenGLPFAColorSize, NSOpenGLPFADepthSize, NSOpenGLPFADoubleBuffer, NSOpenGLPFAMultisample, 13 | NSOpenGLPFAOpenGLProfile, NSOpenGLPFASampleBuffers, NSOpenGLPFASamples, NSOpenGLPFAStencilSize, 14 | NSOpenGLPixelFormat, NSOpenGLProfileVersion3_2Core, NSOpenGLProfileVersion4_1Core, 15 | NSOpenGLProfileVersionLegacy, NSOpenGLView, NSView, 16 | }; 17 | use cocoa::base::{id, nil, YES}; 18 | use cocoa::foundation::NSSize; 19 | 20 | use core_foundation::base::TCFType; 21 | use core_foundation::bundle::{CFBundleGetBundleWithIdentifier, CFBundleGetFunctionPointerForName}; 22 | use core_foundation::string::CFString; 23 | 24 | use objc::{msg_send, sel, sel_impl}; 25 | 26 | use super::{GlConfig, GlError, Profile}; 27 | 28 | pub type CreationFailedError = (); 29 | pub struct GlContext { 30 | view: id, 31 | context: id, 32 | } 33 | 34 | impl GlContext { 35 | pub unsafe fn create(parent: &RawWindowHandle, config: GlConfig) -> Result { 36 | let handle = if let RawWindowHandle::AppKit(handle) = parent { 37 | handle 38 | } else { 39 | return Err(GlError::InvalidWindowHandle); 40 | }; 41 | 42 | if handle.ns_view.is_null() { 43 | return Err(GlError::InvalidWindowHandle); 44 | } 45 | 46 | let parent_view = handle.ns_view as id; 47 | 48 | let version = if config.version < (3, 2) && config.profile == Profile::Compatibility { 49 | NSOpenGLProfileVersionLegacy 50 | } else if config.version == (3, 2) && config.profile == Profile::Core { 51 | NSOpenGLProfileVersion3_2Core 52 | } else if config.version > (3, 2) && config.profile == Profile::Core { 53 | NSOpenGLProfileVersion4_1Core 54 | } else { 55 | return Err(GlError::VersionNotSupported); 56 | }; 57 | 58 | #[rustfmt::skip] 59 | let mut attrs = vec![ 60 | NSOpenGLPFAOpenGLProfile as u32, version as u32, 61 | NSOpenGLPFAColorSize as u32, (config.red_bits + config.blue_bits + config.green_bits) as u32, 62 | NSOpenGLPFAAlphaSize as u32, config.alpha_bits as u32, 63 | NSOpenGLPFADepthSize as u32, config.depth_bits as u32, 64 | NSOpenGLPFAStencilSize as u32, config.stencil_bits as u32, 65 | NSOpenGLPFAAccelerated as u32, 66 | ]; 67 | 68 | if config.samples.is_some() { 69 | #[rustfmt::skip] 70 | attrs.extend_from_slice(&[ 71 | NSOpenGLPFAMultisample as u32, 72 | NSOpenGLPFASampleBuffers as u32, 1, 73 | NSOpenGLPFASamples as u32, config.samples.unwrap() as u32, 74 | ]); 75 | } 76 | 77 | if config.double_buffer { 78 | attrs.push(NSOpenGLPFADoubleBuffer as u32); 79 | } 80 | 81 | attrs.push(0); 82 | 83 | let pixel_format = NSOpenGLPixelFormat::alloc(nil).initWithAttributes_(&attrs); 84 | 85 | if pixel_format == nil { 86 | return Err(GlError::CreationFailed(())); 87 | } 88 | 89 | let view = 90 | NSOpenGLView::alloc(nil).initWithFrame_pixelFormat_(parent_view.frame(), pixel_format); 91 | 92 | if view == nil { 93 | return Err(GlError::CreationFailed(())); 94 | } 95 | 96 | view.setWantsBestResolutionOpenGLSurface_(YES); 97 | 98 | NSOpenGLView::display_(view); 99 | parent_view.addSubview_(view); 100 | 101 | let context: id = msg_send![view, openGLContext]; 102 | let () = msg_send![context, retain]; 103 | 104 | context.setValues_forParameter_( 105 | &(config.vsync as i32), 106 | NSOpenGLContextParameter::NSOpenGLCPSwapInterval, 107 | ); 108 | 109 | let () = msg_send![pixel_format, release]; 110 | 111 | Ok(GlContext { view, context }) 112 | } 113 | 114 | pub unsafe fn make_current(&self) { 115 | self.context.makeCurrentContext(); 116 | } 117 | 118 | pub unsafe fn make_not_current(&self) { 119 | NSOpenGLContext::clearCurrentContext(self.context); 120 | } 121 | 122 | pub fn get_proc_address(&self, symbol: &str) -> *const c_void { 123 | let symbol_name = CFString::from_str(symbol).unwrap(); 124 | let framework_name = CFString::from_str("com.apple.opengl").unwrap(); 125 | let framework = 126 | unsafe { CFBundleGetBundleWithIdentifier(framework_name.as_concrete_TypeRef()) }; 127 | 128 | unsafe { CFBundleGetFunctionPointerForName(framework, symbol_name.as_concrete_TypeRef()) } 129 | } 130 | 131 | pub fn swap_buffers(&self) { 132 | unsafe { 133 | self.context.flushBuffer(); 134 | let () = msg_send![self.view, setNeedsDisplay: YES]; 135 | } 136 | } 137 | 138 | /// On macOS the `NSOpenGLView` needs to be resized separtely from our main view. 139 | pub(crate) fn resize(&self, size: NSSize) { 140 | unsafe { NSView::setFrameSize(self.view, size) }; 141 | unsafe { 142 | let _: () = msg_send![self.view, setNeedsDisplay: YES]; 143 | } 144 | } 145 | } 146 | 147 | impl Drop for GlContext { 148 | fn drop(&mut self) { 149 | unsafe { 150 | let () = msg_send![self.context, release]; 151 | let () = msg_send![self.view, release]; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/gl/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | use std::marker::PhantomData; 3 | 4 | // On X11 creating the context is a two step process 5 | #[cfg(not(target_os = "linux"))] 6 | use raw_window_handle::RawWindowHandle; 7 | 8 | #[cfg(target_os = "windows")] 9 | mod win; 10 | #[cfg(target_os = "windows")] 11 | use win as platform; 12 | 13 | // We need to use this directly within the X11 window creation to negotiate the correct visual 14 | #[cfg(target_os = "linux")] 15 | pub(crate) mod x11; 16 | #[cfg(target_os = "linux")] 17 | pub(crate) use self::x11 as platform; 18 | 19 | #[cfg(target_os = "macos")] 20 | mod macos; 21 | #[cfg(target_os = "macos")] 22 | use macos as platform; 23 | 24 | #[derive(Clone, Debug)] 25 | pub struct GlConfig { 26 | pub version: (u8, u8), 27 | pub profile: Profile, 28 | pub red_bits: u8, 29 | pub blue_bits: u8, 30 | pub green_bits: u8, 31 | pub alpha_bits: u8, 32 | pub depth_bits: u8, 33 | pub stencil_bits: u8, 34 | pub samples: Option, 35 | pub srgb: bool, 36 | pub double_buffer: bool, 37 | pub vsync: bool, 38 | } 39 | 40 | impl Default for GlConfig { 41 | fn default() -> Self { 42 | GlConfig { 43 | version: (3, 2), 44 | profile: Profile::Core, 45 | red_bits: 8, 46 | blue_bits: 8, 47 | green_bits: 8, 48 | alpha_bits: 8, 49 | depth_bits: 24, 50 | stencil_bits: 8, 51 | samples: None, 52 | srgb: true, 53 | double_buffer: true, 54 | vsync: false, 55 | } 56 | } 57 | } 58 | 59 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 60 | pub enum Profile { 61 | Compatibility, 62 | Core, 63 | } 64 | 65 | #[derive(Debug)] 66 | pub enum GlError { 67 | InvalidWindowHandle, 68 | VersionNotSupported, 69 | CreationFailed(platform::CreationFailedError), 70 | } 71 | 72 | pub struct GlContext { 73 | context: platform::GlContext, 74 | phantom: PhantomData<*mut ()>, 75 | } 76 | 77 | impl GlContext { 78 | #[cfg(not(target_os = "linux"))] 79 | pub(crate) unsafe fn create( 80 | parent: &RawWindowHandle, config: GlConfig, 81 | ) -> Result { 82 | platform::GlContext::create(parent, config) 83 | .map(|context| GlContext { context, phantom: PhantomData }) 84 | } 85 | 86 | /// The X11 version needs to be set up in a different way compared to the Windows and macOS 87 | /// versions. So the platform-specific versions should be used to construct the context within 88 | /// baseview, and then this object can be passed to the user. 89 | #[cfg(target_os = "linux")] 90 | pub(crate) fn new(context: platform::GlContext) -> GlContext { 91 | GlContext { context, phantom: PhantomData } 92 | } 93 | 94 | pub unsafe fn make_current(&self) { 95 | self.context.make_current(); 96 | } 97 | 98 | pub unsafe fn make_not_current(&self) { 99 | self.context.make_not_current(); 100 | } 101 | 102 | pub fn get_proc_address(&self, symbol: &str) -> *const c_void { 103 | self.context.get_proc_address(symbol) 104 | } 105 | 106 | pub fn swap_buffers(&self) { 107 | self.context.swap_buffers(); 108 | } 109 | 110 | /// On macOS the `NSOpenGLView` needs to be resized separtely from our main view. 111 | #[cfg(target_os = "macos")] 112 | pub(crate) fn resize(&self, size: cocoa::foundation::NSSize) { 113 | self.context.resize(size); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/gl/win.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_void, CString, OsStr}; 2 | use std::os::windows::ffi::OsStrExt; 3 | 4 | use raw_window_handle::RawWindowHandle; 5 | 6 | use winapi::shared::minwindef::{HINSTANCE, HMODULE}; 7 | use winapi::shared::ntdef::WCHAR; 8 | use winapi::shared::windef::{HDC, HGLRC, HWND}; 9 | use winapi::um::libloaderapi::{FreeLibrary, GetProcAddress, LoadLibraryA}; 10 | use winapi::um::wingdi::{ 11 | wglCreateContext, wglDeleteContext, wglGetProcAddress, wglMakeCurrent, ChoosePixelFormat, 12 | DescribePixelFormat, SetPixelFormat, SwapBuffers, PFD_DOUBLEBUFFER, PFD_DRAW_TO_WINDOW, 13 | PFD_MAIN_PLANE, PFD_SUPPORT_OPENGL, PFD_TYPE_RGBA, PIXELFORMATDESCRIPTOR, 14 | }; 15 | use winapi::um::winnt::IMAGE_DOS_HEADER; 16 | use winapi::um::winuser::{ 17 | CreateWindowExW, DefWindowProcW, DestroyWindow, GetDC, RegisterClassW, ReleaseDC, 18 | UnregisterClassW, CS_OWNDC, CW_USEDEFAULT, WNDCLASSW, 19 | }; 20 | 21 | use super::{GlConfig, GlError, Profile}; 22 | 23 | // See https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_create_context.txt 24 | 25 | type WglCreateContextAttribsARB = extern "system" fn(HDC, HGLRC, *const i32) -> HGLRC; 26 | 27 | const WGL_CONTEXT_MAJOR_VERSION_ARB: i32 = 0x2091; 28 | const WGL_CONTEXT_MINOR_VERSION_ARB: i32 = 0x2092; 29 | const WGL_CONTEXT_PROFILE_MASK_ARB: i32 = 0x9126; 30 | 31 | const WGL_CONTEXT_CORE_PROFILE_BIT_ARB: i32 = 0x00000001; 32 | const WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB: i32 = 0x00000002; 33 | 34 | // See https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_pixel_format.txt 35 | 36 | type WglChoosePixelFormatARB = 37 | extern "system" fn(HDC, *const i32, *const f32, u32, *mut i32, *mut u32) -> i32; 38 | 39 | const WGL_DRAW_TO_WINDOW_ARB: i32 = 0x2001; 40 | const WGL_ACCELERATION_ARB: i32 = 0x2003; 41 | const WGL_SUPPORT_OPENGL_ARB: i32 = 0x2010; 42 | const WGL_DOUBLE_BUFFER_ARB: i32 = 0x2011; 43 | const WGL_PIXEL_TYPE_ARB: i32 = 0x2013; 44 | const WGL_RED_BITS_ARB: i32 = 0x2015; 45 | const WGL_GREEN_BITS_ARB: i32 = 0x2017; 46 | const WGL_BLUE_BITS_ARB: i32 = 0x2019; 47 | const WGL_ALPHA_BITS_ARB: i32 = 0x201B; 48 | const WGL_DEPTH_BITS_ARB: i32 = 0x2022; 49 | const WGL_STENCIL_BITS_ARB: i32 = 0x2023; 50 | 51 | const WGL_FULL_ACCELERATION_ARB: i32 = 0x2027; 52 | const WGL_TYPE_RGBA_ARB: i32 = 0x202B; 53 | 54 | // See https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_multisample.txt 55 | 56 | const WGL_SAMPLE_BUFFERS_ARB: i32 = 0x2041; 57 | const WGL_SAMPLES_ARB: i32 = 0x2042; 58 | 59 | // See https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_framebuffer_sRGB.txt 60 | 61 | const WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB: i32 = 0x20A9; 62 | 63 | // See https://www.khronos.org/registry/OpenGL/extensions/EXT/WGL_EXT_swap_control.txt 64 | 65 | type WglSwapIntervalEXT = extern "system" fn(i32) -> i32; 66 | 67 | pub type CreationFailedError = (); 68 | pub struct GlContext { 69 | hwnd: HWND, 70 | hdc: HDC, 71 | hglrc: HGLRC, 72 | gl_library: HMODULE, 73 | } 74 | 75 | extern "C" { 76 | static __ImageBase: IMAGE_DOS_HEADER; 77 | } 78 | 79 | impl GlContext { 80 | pub unsafe fn create(parent: &RawWindowHandle, config: GlConfig) -> Result { 81 | let handle = if let RawWindowHandle::Win32(handle) = parent { 82 | handle 83 | } else { 84 | return Err(GlError::InvalidWindowHandle); 85 | }; 86 | 87 | if handle.hwnd.is_null() { 88 | return Err(GlError::InvalidWindowHandle); 89 | } 90 | 91 | // Create temporary window and context to load function pointers 92 | 93 | let class_name_str = format!("raw-gl-context-window-{}", uuid::Uuid::new_v4().to_simple()); 94 | let mut class_name: Vec = OsStr::new(&class_name_str).encode_wide().collect(); 95 | class_name.push(0); 96 | 97 | let hinstance = &__ImageBase as *const IMAGE_DOS_HEADER as HINSTANCE; 98 | 99 | let wnd_class = WNDCLASSW { 100 | style: CS_OWNDC, 101 | lpfnWndProc: Some(DefWindowProcW), 102 | hInstance: hinstance, 103 | lpszClassName: class_name.as_ptr(), 104 | ..std::mem::zeroed() 105 | }; 106 | 107 | let class = RegisterClassW(&wnd_class); 108 | if class == 0 { 109 | return Err(GlError::CreationFailed(())); 110 | } 111 | 112 | let hwnd_tmp = CreateWindowExW( 113 | 0, 114 | class as *const WCHAR, 115 | [0].as_ptr(), 116 | 0, 117 | CW_USEDEFAULT, 118 | CW_USEDEFAULT, 119 | CW_USEDEFAULT, 120 | CW_USEDEFAULT, 121 | std::ptr::null_mut(), 122 | std::ptr::null_mut(), 123 | hinstance, 124 | std::ptr::null_mut(), 125 | ); 126 | 127 | if hwnd_tmp.is_null() { 128 | return Err(GlError::CreationFailed(())); 129 | } 130 | 131 | let hdc_tmp = GetDC(hwnd_tmp); 132 | 133 | let pfd_tmp = PIXELFORMATDESCRIPTOR { 134 | nSize: std::mem::size_of::() as u16, 135 | nVersion: 1, 136 | dwFlags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, 137 | iPixelType: PFD_TYPE_RGBA, 138 | cColorBits: 32, 139 | cAlphaBits: 8, 140 | cDepthBits: 24, 141 | cStencilBits: 8, 142 | iLayerType: PFD_MAIN_PLANE, 143 | ..std::mem::zeroed() 144 | }; 145 | 146 | SetPixelFormat(hdc_tmp, ChoosePixelFormat(hdc_tmp, &pfd_tmp), &pfd_tmp); 147 | 148 | let hglrc_tmp = wglCreateContext(hdc_tmp); 149 | if hglrc_tmp.is_null() { 150 | ReleaseDC(hwnd_tmp, hdc_tmp); 151 | UnregisterClassW(class as *const WCHAR, hinstance); 152 | DestroyWindow(hwnd_tmp); 153 | return Err(GlError::CreationFailed(())); 154 | } 155 | 156 | wglMakeCurrent(hdc_tmp, hglrc_tmp); 157 | 158 | #[allow(non_snake_case)] 159 | let wglCreateContextAttribsARB: Option = { 160 | let symbol = CString::new("wglCreateContextAttribsARB").unwrap(); 161 | let addr = wglGetProcAddress(symbol.as_ptr()); 162 | if !addr.is_null() { 163 | #[allow(clippy::missing_transmute_annotations)] 164 | Some(std::mem::transmute(addr)) 165 | } else { 166 | None 167 | } 168 | }; 169 | 170 | #[allow(non_snake_case)] 171 | let wglChoosePixelFormatARB: Option = { 172 | let symbol = CString::new("wglChoosePixelFormatARB").unwrap(); 173 | let addr = wglGetProcAddress(symbol.as_ptr()); 174 | if !addr.is_null() { 175 | #[allow(clippy::missing_transmute_annotations)] 176 | Some(std::mem::transmute(addr)) 177 | } else { 178 | None 179 | } 180 | }; 181 | 182 | #[allow(non_snake_case)] 183 | let wglSwapIntervalEXT: Option = { 184 | let symbol = CString::new("wglSwapIntervalEXT").unwrap(); 185 | let addr = wglGetProcAddress(symbol.as_ptr()); 186 | if !addr.is_null() { 187 | #[allow(clippy::missing_transmute_annotations)] 188 | Some(std::mem::transmute(addr)) 189 | } else { 190 | None 191 | } 192 | }; 193 | 194 | wglMakeCurrent(hdc_tmp, std::ptr::null_mut()); 195 | wglDeleteContext(hglrc_tmp); 196 | ReleaseDC(hwnd_tmp, hdc_tmp); 197 | UnregisterClassW(class as *const WCHAR, hinstance); 198 | DestroyWindow(hwnd_tmp); 199 | 200 | // Create actual context 201 | 202 | let hwnd = handle.hwnd as HWND; 203 | 204 | let hdc = GetDC(hwnd); 205 | 206 | #[rustfmt::skip] 207 | let pixel_format_attribs = [ 208 | WGL_DRAW_TO_WINDOW_ARB, 1, 209 | WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB, 210 | WGL_SUPPORT_OPENGL_ARB, 1, 211 | WGL_DOUBLE_BUFFER_ARB, config.double_buffer as i32, 212 | WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB, 213 | WGL_RED_BITS_ARB, config.red_bits as i32, 214 | WGL_GREEN_BITS_ARB, config.green_bits as i32, 215 | WGL_BLUE_BITS_ARB, config.blue_bits as i32, 216 | WGL_ALPHA_BITS_ARB, config.alpha_bits as i32, 217 | WGL_DEPTH_BITS_ARB, config.depth_bits as i32, 218 | WGL_STENCIL_BITS_ARB, config.stencil_bits as i32, 219 | WGL_SAMPLE_BUFFERS_ARB, config.samples.is_some() as i32, 220 | WGL_SAMPLES_ARB, config.samples.unwrap_or(0) as i32, 221 | WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB, config.srgb as i32, 222 | 0, 223 | ]; 224 | 225 | let mut pixel_format = 0; 226 | let mut num_formats = 0; 227 | wglChoosePixelFormatARB.unwrap()( 228 | hdc, 229 | pixel_format_attribs.as_ptr(), 230 | std::ptr::null(), 231 | 1, 232 | &mut pixel_format, 233 | &mut num_formats, 234 | ); 235 | 236 | let mut pfd: PIXELFORMATDESCRIPTOR = std::mem::zeroed(); 237 | DescribePixelFormat( 238 | hdc, 239 | pixel_format, 240 | std::mem::size_of::() as u32, 241 | &mut pfd, 242 | ); 243 | SetPixelFormat(hdc, pixel_format, &pfd); 244 | 245 | let profile_mask = match config.profile { 246 | Profile::Core => WGL_CONTEXT_CORE_PROFILE_BIT_ARB, 247 | Profile::Compatibility => WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB, 248 | }; 249 | 250 | #[rustfmt::skip] 251 | let ctx_attribs = [ 252 | WGL_CONTEXT_MAJOR_VERSION_ARB, config.version.0 as i32, 253 | WGL_CONTEXT_MINOR_VERSION_ARB, config.version.1 as i32, 254 | WGL_CONTEXT_PROFILE_MASK_ARB, profile_mask, 255 | 0 256 | ]; 257 | 258 | let hglrc = 259 | wglCreateContextAttribsARB.unwrap()(hdc, std::ptr::null_mut(), ctx_attribs.as_ptr()); 260 | if hglrc.is_null() { 261 | return Err(GlError::CreationFailed(())); 262 | } 263 | 264 | let gl_library_name = CString::new("opengl32.dll").unwrap(); 265 | let gl_library = LoadLibraryA(gl_library_name.as_ptr()); 266 | 267 | wglMakeCurrent(hdc, hglrc); 268 | wglSwapIntervalEXT.unwrap()(config.vsync as i32); 269 | wglMakeCurrent(hdc, std::ptr::null_mut()); 270 | 271 | Ok(GlContext { hwnd, hdc, hglrc, gl_library }) 272 | } 273 | 274 | pub unsafe fn make_current(&self) { 275 | wglMakeCurrent(self.hdc, self.hglrc); 276 | } 277 | 278 | pub unsafe fn make_not_current(&self) { 279 | wglMakeCurrent(self.hdc, std::ptr::null_mut()); 280 | } 281 | 282 | pub fn get_proc_address(&self, symbol: &str) -> *const c_void { 283 | let symbol = CString::new(symbol).unwrap(); 284 | let addr = unsafe { wglGetProcAddress(symbol.as_ptr()) as *const c_void }; 285 | if !addr.is_null() { 286 | addr 287 | } else { 288 | unsafe { GetProcAddress(self.gl_library, symbol.as_ptr()) as *const c_void } 289 | } 290 | } 291 | 292 | pub fn swap_buffers(&self) { 293 | unsafe { 294 | SwapBuffers(self.hdc); 295 | } 296 | } 297 | } 298 | 299 | impl Drop for GlContext { 300 | fn drop(&mut self) { 301 | unsafe { 302 | wglMakeCurrent(std::ptr::null_mut(), std::ptr::null_mut()); 303 | wglDeleteContext(self.hglrc); 304 | ReleaseDC(self.hwnd, self.hdc); 305 | FreeLibrary(self.gl_library); 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/gl/x11.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_void, CString}; 2 | use std::os::raw::{c_int, c_ulong}; 3 | 4 | use x11::glx; 5 | use x11::xlib; 6 | 7 | use super::{GlConfig, GlError, Profile}; 8 | 9 | mod errors; 10 | 11 | #[derive(Debug)] 12 | pub enum CreationFailedError { 13 | InvalidFBConfig, 14 | NoVisual, 15 | GetProcAddressFailed, 16 | MakeCurrentFailed, 17 | ContextCreationFailed, 18 | X11Error(errors::XLibError), 19 | } 20 | 21 | impl From for GlError { 22 | fn from(e: errors::XLibError) -> Self { 23 | GlError::CreationFailed(CreationFailedError::X11Error(e)) 24 | } 25 | } 26 | 27 | // See https://www.khronos.org/registry/OpenGL/extensions/ARB/GLX_ARB_create_context.txt 28 | 29 | type GlXCreateContextAttribsARB = unsafe extern "C" fn( 30 | dpy: *mut xlib::Display, 31 | fbc: glx::GLXFBConfig, 32 | share_context: glx::GLXContext, 33 | direct: xlib::Bool, 34 | attribs: *const c_int, 35 | ) -> glx::GLXContext; 36 | 37 | // See https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_swap_control.txt 38 | 39 | type GlXSwapIntervalEXT = 40 | unsafe extern "C" fn(dpy: *mut xlib::Display, drawable: glx::GLXDrawable, interval: i32); 41 | 42 | // See https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_framebuffer_sRGB.txt 43 | 44 | const GLX_FRAMEBUFFER_SRGB_CAPABLE_ARB: i32 = 0x20B2; 45 | 46 | fn get_proc_address(symbol: &str) -> *const c_void { 47 | let symbol = CString::new(symbol).unwrap(); 48 | unsafe { glx::glXGetProcAddress(symbol.as_ptr() as *const u8).unwrap() as *const c_void } 49 | } 50 | 51 | pub struct GlContext { 52 | window: c_ulong, 53 | display: *mut xlib::_XDisplay, 54 | context: glx::GLXContext, 55 | } 56 | 57 | /// The frame buffer configuration along with the general OpenGL configuration to somewhat minimize 58 | /// misuse. 59 | pub struct FbConfig { 60 | gl_config: GlConfig, 61 | fb_config: *mut glx::__GLXFBConfigRec, 62 | } 63 | 64 | /// The configuration a window should be created with after calling 65 | /// [GlContext::get_fb_config_and_visual]. 66 | pub struct WindowConfig { 67 | pub depth: u8, 68 | pub visual: u32, 69 | } 70 | 71 | impl GlContext { 72 | /// Creating an OpenGL context under X11 works slightly different. Different OpenGL 73 | /// configurations require different framebuffer configurations, and to be able to use that 74 | /// context with a window the window needs to be created with a matching visual. This means that 75 | /// you need to decide on the framebuffer config before creating the window, ask the X11 server 76 | /// for a matching visual for that framebuffer config, crate the window with that visual, and 77 | /// only then create the OpenGL context. 78 | /// 79 | /// Use [Self::get_fb_config_and_visual] to create both of these things. 80 | pub unsafe fn create( 81 | window: c_ulong, display: *mut xlib::_XDisplay, config: FbConfig, 82 | ) -> Result { 83 | if display.is_null() { 84 | return Err(GlError::InvalidWindowHandle); 85 | } 86 | 87 | errors::XErrorHandler::handle(display, |error_handler| { 88 | #[allow(non_snake_case)] 89 | let glXCreateContextAttribsARB: GlXCreateContextAttribsARB = { 90 | let addr = get_proc_address("glXCreateContextAttribsARB"); 91 | if addr.is_null() { 92 | return Err(GlError::CreationFailed(CreationFailedError::GetProcAddressFailed)); 93 | } else { 94 | #[allow(clippy::missing_transmute_annotations)] 95 | std::mem::transmute(addr) 96 | } 97 | }; 98 | 99 | #[allow(non_snake_case)] 100 | let glXSwapIntervalEXT: GlXSwapIntervalEXT = { 101 | let addr = get_proc_address("glXSwapIntervalEXT"); 102 | if addr.is_null() { 103 | return Err(GlError::CreationFailed(CreationFailedError::GetProcAddressFailed)); 104 | } else { 105 | #[allow(clippy::missing_transmute_annotations)] 106 | std::mem::transmute(addr) 107 | } 108 | }; 109 | 110 | error_handler.check()?; 111 | 112 | let profile_mask = match config.gl_config.profile { 113 | Profile::Core => glx::arb::GLX_CONTEXT_CORE_PROFILE_BIT_ARB, 114 | Profile::Compatibility => glx::arb::GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB, 115 | }; 116 | 117 | #[rustfmt::skip] 118 | let ctx_attribs = [ 119 | glx::arb::GLX_CONTEXT_MAJOR_VERSION_ARB, config.gl_config.version.0 as i32, 120 | glx::arb::GLX_CONTEXT_MINOR_VERSION_ARB, config.gl_config.version.1 as i32, 121 | glx::arb::GLX_CONTEXT_PROFILE_MASK_ARB, profile_mask, 122 | 0, 123 | ]; 124 | 125 | let context = glXCreateContextAttribsARB( 126 | display, 127 | config.fb_config, 128 | std::ptr::null_mut(), 129 | 1, 130 | ctx_attribs.as_ptr(), 131 | ); 132 | 133 | error_handler.check()?; 134 | 135 | if context.is_null() { 136 | return Err(GlError::CreationFailed(CreationFailedError::ContextCreationFailed)); 137 | } 138 | 139 | let res = glx::glXMakeCurrent(display, window, context); 140 | error_handler.check()?; 141 | if res == 0 { 142 | return Err(GlError::CreationFailed(CreationFailedError::MakeCurrentFailed)); 143 | } 144 | 145 | glXSwapIntervalEXT(display, window, config.gl_config.vsync as i32); 146 | error_handler.check()?; 147 | 148 | if glx::glXMakeCurrent(display, 0, std::ptr::null_mut()) == 0 { 149 | error_handler.check()?; 150 | return Err(GlError::CreationFailed(CreationFailedError::MakeCurrentFailed)); 151 | } 152 | 153 | Ok(GlContext { window, display, context }) 154 | }) 155 | } 156 | 157 | /// Find a matching framebuffer config and window visual for the given OpenGL configuration. 158 | /// This needs to be passed to [Self::create] along with a handle to a window that was created 159 | /// using the visual also returned from this function. 160 | pub unsafe fn get_fb_config_and_visual( 161 | display: *mut xlib::_XDisplay, config: GlConfig, 162 | ) -> Result<(FbConfig, WindowConfig), GlError> { 163 | errors::XErrorHandler::handle(display, |error_handler| { 164 | let screen = xlib::XDefaultScreen(display); 165 | 166 | #[rustfmt::skip] 167 | let fb_attribs = [ 168 | glx::GLX_X_RENDERABLE, 1, 169 | glx::GLX_X_VISUAL_TYPE, glx::GLX_TRUE_COLOR, 170 | glx::GLX_DRAWABLE_TYPE, glx::GLX_WINDOW_BIT, 171 | glx::GLX_RENDER_TYPE, glx::GLX_RGBA_BIT, 172 | glx::GLX_RED_SIZE, config.red_bits as i32, 173 | glx::GLX_GREEN_SIZE, config.green_bits as i32, 174 | glx::GLX_BLUE_SIZE, config.blue_bits as i32, 175 | glx::GLX_ALPHA_SIZE, config.alpha_bits as i32, 176 | glx::GLX_DEPTH_SIZE, config.depth_bits as i32, 177 | glx::GLX_STENCIL_SIZE, config.stencil_bits as i32, 178 | glx::GLX_DOUBLEBUFFER, config.double_buffer as i32, 179 | glx::GLX_SAMPLE_BUFFERS, config.samples.is_some() as i32, 180 | glx::GLX_SAMPLES, config.samples.unwrap_or(0) as i32, 181 | GLX_FRAMEBUFFER_SRGB_CAPABLE_ARB, config.srgb as i32, 182 | 0, 183 | ]; 184 | 185 | let mut n_configs = 0; 186 | let fb_config = 187 | glx::glXChooseFBConfig(display, screen, fb_attribs.as_ptr(), &mut n_configs); 188 | 189 | error_handler.check()?; 190 | if n_configs <= 0 || fb_config.is_null() { 191 | return Err(GlError::CreationFailed(CreationFailedError::InvalidFBConfig)); 192 | } 193 | 194 | // Now that we have a matching framebuffer config, we need to know which visual matches 195 | // thsi config so the window is compatible with the OpenGL context we're about to create 196 | let fb_config = *fb_config; 197 | let visual = glx::glXGetVisualFromFBConfig(display, fb_config); 198 | if visual.is_null() { 199 | return Err(GlError::CreationFailed(CreationFailedError::NoVisual)); 200 | } 201 | 202 | Ok(( 203 | FbConfig { fb_config, gl_config: config }, 204 | WindowConfig { depth: (*visual).depth as u8, visual: (*visual).visualid as u32 }, 205 | )) 206 | }) 207 | } 208 | 209 | pub unsafe fn make_current(&self) { 210 | errors::XErrorHandler::handle(self.display, |error_handler| { 211 | let res = glx::glXMakeCurrent(self.display, self.window, self.context); 212 | error_handler.check().unwrap(); 213 | if res == 0 { 214 | panic!("make_current failed") 215 | } 216 | }) 217 | } 218 | 219 | pub unsafe fn make_not_current(&self) { 220 | errors::XErrorHandler::handle(self.display, |error_handler| { 221 | let res = glx::glXMakeCurrent(self.display, 0, std::ptr::null_mut()); 222 | error_handler.check().unwrap(); 223 | if res == 0 { 224 | panic!("make_not_current failed") 225 | } 226 | }) 227 | } 228 | 229 | pub fn get_proc_address(&self, symbol: &str) -> *const c_void { 230 | get_proc_address(symbol) 231 | } 232 | 233 | pub fn swap_buffers(&self) { 234 | unsafe { 235 | errors::XErrorHandler::handle(self.display, |error_handler| { 236 | glx::glXSwapBuffers(self.display, self.window); 237 | error_handler.check().unwrap(); 238 | }) 239 | } 240 | } 241 | } 242 | 243 | impl Drop for GlContext { 244 | fn drop(&mut self) {} 245 | } 246 | -------------------------------------------------------------------------------- /src/gl/x11/errors.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | use std::fmt::{Debug, Display, Formatter}; 3 | use x11::xlib; 4 | 5 | use std::cell::RefCell; 6 | use std::error::Error; 7 | use std::os::raw::{c_int, c_uchar, c_ulong}; 8 | use std::panic::AssertUnwindSafe; 9 | 10 | thread_local! { 11 | /// Used as part of [`XErrorHandler::handle()`]. When an X11 error occurs during this function, 12 | /// the error gets copied to this RefCell after which the program is allowed to resume. The 13 | /// error can then be converted to a regular Rust Result value afterward. 14 | static CURRENT_X11_ERROR: RefCell> = const { RefCell::new(None) }; 15 | } 16 | 17 | /// A helper struct for safe X11 error handling 18 | pub struct XErrorHandler<'a> { 19 | display: *mut xlib::Display, 20 | error: &'a RefCell>, 21 | } 22 | 23 | impl<'a> XErrorHandler<'a> { 24 | /// Syncs and checks if any previous X11 calls from the given display returned an error 25 | pub fn check(&mut self) -> Result<(), XLibError> { 26 | // Flush all possible previous errors 27 | unsafe { 28 | xlib::XSync(self.display, 0); 29 | } 30 | let error = self.error.borrow_mut().take(); 31 | 32 | match error { 33 | None => Ok(()), 34 | Some(inner) => Err(inner), 35 | } 36 | } 37 | 38 | /// Sets up a temporary X11 error handler for the duration of the given closure, and allows 39 | /// that closure to check on the latest X11 error at any time. 40 | /// 41 | /// # Safety 42 | /// 43 | /// The given display pointer *must* be and remain valid for the duration of this function, as 44 | /// well as for the duration of the given `handler` closure. 45 | pub unsafe fn handle T>( 46 | display: *mut xlib::Display, handler: F, 47 | ) -> T { 48 | /// # Safety 49 | /// The given display and error pointers *must* be valid for the duration of this function. 50 | unsafe extern "C" fn error_handler( 51 | _dpy: *mut xlib::Display, err: *mut xlib::XErrorEvent, 52 | ) -> i32 { 53 | // SAFETY: the error pointer should be safe to access 54 | let err = &*err; 55 | 56 | CURRENT_X11_ERROR.with(|error| { 57 | let mut error = error.borrow_mut(); 58 | match error.as_mut() { 59 | // If multiple errors occur, keep the first one since that's likely going to be the 60 | // cause of the other errors 61 | Some(_) => 1, 62 | None => { 63 | *error = Some(XLibError::from_event(err)); 64 | 0 65 | } 66 | } 67 | }) 68 | } 69 | 70 | // Flush all possible previous errors 71 | unsafe { 72 | xlib::XSync(display, 0); 73 | } 74 | 75 | CURRENT_X11_ERROR.with(|error| { 76 | // Make sure to clear any errors from the last call to this function 77 | { 78 | *error.borrow_mut() = None; 79 | } 80 | 81 | let old_handler = unsafe { xlib::XSetErrorHandler(Some(error_handler)) }; 82 | let panic_result = std::panic::catch_unwind(AssertUnwindSafe(|| { 83 | let mut h = XErrorHandler { display, error }; 84 | handler(&mut h) 85 | })); 86 | // Whatever happened, restore old error handler 87 | unsafe { xlib::XSetErrorHandler(old_handler) }; 88 | 89 | match panic_result { 90 | Ok(v) => v, 91 | Err(e) => std::panic::resume_unwind(e), 92 | } 93 | }) 94 | } 95 | } 96 | 97 | pub struct XLibError { 98 | type_: c_int, 99 | resourceid: xlib::XID, 100 | serial: c_ulong, 101 | error_code: c_uchar, 102 | request_code: c_uchar, 103 | minor_code: c_uchar, 104 | 105 | display_name: Box, 106 | } 107 | 108 | impl XLibError { 109 | /// # Safety 110 | /// The display pointer inside error must be valid for the duration of this call 111 | unsafe fn from_event(error: &xlib::XErrorEvent) -> Self { 112 | Self { 113 | type_: error.type_, 114 | resourceid: error.resourceid, 115 | serial: error.serial, 116 | 117 | error_code: error.error_code, 118 | request_code: error.request_code, 119 | minor_code: error.minor_code, 120 | 121 | display_name: Self::get_display_name(error), 122 | } 123 | } 124 | 125 | /// # Safety 126 | /// The display pointer inside error must be valid for the duration of this call 127 | unsafe fn get_display_name(error: &xlib::XErrorEvent) -> Box { 128 | let mut buf = [0; 255]; 129 | unsafe { 130 | xlib::XGetErrorText( 131 | error.display, 132 | error.error_code.into(), 133 | buf.as_mut_ptr().cast(), 134 | (buf.len() - 1) as i32, 135 | ); 136 | } 137 | 138 | *buf.last_mut().unwrap() = 0; 139 | // SAFETY: whatever XGetErrorText did or not, we guaranteed there is a nul byte at the end of the buffer 140 | let cstr = unsafe { CStr::from_ptr(buf.as_mut_ptr().cast()) }; 141 | 142 | cstr.to_string_lossy().into() 143 | } 144 | } 145 | 146 | impl Debug for XLibError { 147 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 148 | f.debug_struct("XLibError") 149 | .field("error_code", &self.error_code) 150 | .field("error_message", &self.display_name) 151 | .field("minor_code", &self.minor_code) 152 | .field("request_code", &self.request_code) 153 | .field("type", &self.type_) 154 | .field("resource_id", &self.resourceid) 155 | .field("serial", &self.serial) 156 | .finish() 157 | } 158 | } 159 | 160 | impl Display for XLibError { 161 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 162 | write!(f, "XLib error: {} (error code {})", &self.display_name, self.error_code) 163 | } 164 | } 165 | 166 | impl Error for XLibError {} 167 | -------------------------------------------------------------------------------- /src/keyboard.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Druid Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Baseview modifications to druid code: 16 | // - only keep code_to_location function 17 | 18 | //! Keyboard types. 19 | 20 | #[cfg(any(target_os = "linux", target_os = "macos"))] 21 | use keyboard_types::{Code, Location}; 22 | 23 | #[cfg(any(target_os = "linux", target_os = "macos"))] 24 | /// Map key code to location. 25 | /// 26 | /// The logic for this is adapted from InitKeyEvent in TextInputHandler (in the Mozilla 27 | /// mac port). 28 | /// 29 | /// Note: in the original, this is based on kVK constants, but since we don't have those 30 | /// readily available, we use the mapping to code (which should be effectively lossless). 31 | pub fn code_to_location(code: Code) -> Location { 32 | match code { 33 | Code::MetaLeft | Code::ShiftLeft | Code::AltLeft | Code::ControlLeft => Location::Left, 34 | Code::MetaRight | Code::ShiftRight | Code::AltRight | Code::ControlRight => Location::Right, 35 | Code::Numpad0 36 | | Code::Numpad1 37 | | Code::Numpad2 38 | | Code::Numpad3 39 | | Code::Numpad4 40 | | Code::Numpad5 41 | | Code::Numpad6 42 | | Code::Numpad7 43 | | Code::Numpad8 44 | | Code::Numpad9 45 | | Code::NumpadAdd 46 | | Code::NumpadComma 47 | | Code::NumpadDecimal 48 | | Code::NumpadDivide 49 | | Code::NumpadEnter 50 | | Code::NumpadEqual 51 | | Code::NumpadMultiply 52 | | Code::NumpadSubtract => Location::Numpad, 53 | _ => Location::Standard, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | mod macos; 3 | #[cfg(target_os = "windows")] 4 | mod win; 5 | #[cfg(target_os = "linux")] 6 | mod x11; 7 | 8 | mod clipboard; 9 | mod event; 10 | mod keyboard; 11 | mod mouse_cursor; 12 | mod window; 13 | mod window_info; 14 | mod window_open_options; 15 | 16 | #[cfg(feature = "opengl")] 17 | pub mod gl; 18 | 19 | pub use clipboard::*; 20 | pub use event::*; 21 | pub use mouse_cursor::MouseCursor; 22 | pub use window::*; 23 | pub use window_info::*; 24 | pub use window_open_options::*; 25 | -------------------------------------------------------------------------------- /src/macos/keyboard.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Druid Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Baseview modifications to druid code: 16 | // - move from_nsstring function to this file 17 | // - update imports, paths etc 18 | 19 | //! Conversion of platform keyboard event into cross-platform event. 20 | 21 | use std::cell::Cell; 22 | 23 | use cocoa::appkit::{NSEvent, NSEventModifierFlags, NSEventType}; 24 | use cocoa::base::id; 25 | use cocoa::foundation::NSString; 26 | use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Modifiers}; 27 | use objc::{msg_send, sel, sel_impl}; 28 | 29 | use crate::keyboard::code_to_location; 30 | 31 | pub(crate) fn from_nsstring(s: id) -> String { 32 | unsafe { 33 | let slice = std::slice::from_raw_parts(s.UTF8String() as *const _, s.len()); 34 | let result = std::str::from_utf8_unchecked(slice); 35 | result.into() 36 | } 37 | } 38 | 39 | /// State for processing of keyboard events. 40 | /// 41 | /// This needs to be stateful for proper processing of dead keys. The current 42 | /// implementation is somewhat primitive and is not based on IME; in the future 43 | /// when IME is implemented, it will need to be redone somewhat, letting the IME 44 | /// be the authoritative source of truth for Unicode string values of keys. 45 | /// 46 | /// Most of the logic in this module is adapted from Mozilla, and in particular 47 | /// TextInputHandler.mm. 48 | pub(crate) struct KeyboardState { 49 | last_mods: Cell, 50 | } 51 | 52 | /// Convert a macOS platform key code (keyCode field of NSEvent). 53 | /// 54 | /// The primary source for this mapping is: 55 | /// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values 56 | /// 57 | /// It should also match up with CODE_MAP_MAC bindings in 58 | /// NativeKeyToDOMCodeName.h. 59 | fn key_code_to_code(key_code: u16) -> Code { 60 | match key_code { 61 | 0x00 => Code::KeyA, 62 | 0x01 => Code::KeyS, 63 | 0x02 => Code::KeyD, 64 | 0x03 => Code::KeyF, 65 | 0x04 => Code::KeyH, 66 | 0x05 => Code::KeyG, 67 | 0x06 => Code::KeyZ, 68 | 0x07 => Code::KeyX, 69 | 0x08 => Code::KeyC, 70 | 0x09 => Code::KeyV, 71 | 0x0a => Code::IntlBackslash, 72 | 0x0b => Code::KeyB, 73 | 0x0c => Code::KeyQ, 74 | 0x0d => Code::KeyW, 75 | 0x0e => Code::KeyE, 76 | 0x0f => Code::KeyR, 77 | 0x10 => Code::KeyY, 78 | 0x11 => Code::KeyT, 79 | 0x12 => Code::Digit1, 80 | 0x13 => Code::Digit2, 81 | 0x14 => Code::Digit3, 82 | 0x15 => Code::Digit4, 83 | 0x16 => Code::Digit6, 84 | 0x17 => Code::Digit5, 85 | 0x18 => Code::Equal, 86 | 0x19 => Code::Digit9, 87 | 0x1a => Code::Digit7, 88 | 0x1b => Code::Minus, 89 | 0x1c => Code::Digit8, 90 | 0x1d => Code::Digit0, 91 | 0x1e => Code::BracketRight, 92 | 0x1f => Code::KeyO, 93 | 0x20 => Code::KeyU, 94 | 0x21 => Code::BracketLeft, 95 | 0x22 => Code::KeyI, 96 | 0x23 => Code::KeyP, 97 | 0x24 => Code::Enter, 98 | 0x25 => Code::KeyL, 99 | 0x26 => Code::KeyJ, 100 | 0x27 => Code::Quote, 101 | 0x28 => Code::KeyK, 102 | 0x29 => Code::Semicolon, 103 | 0x2a => Code::Backslash, 104 | 0x2b => Code::Comma, 105 | 0x2c => Code::Slash, 106 | 0x2d => Code::KeyN, 107 | 0x2e => Code::KeyM, 108 | 0x2f => Code::Period, 109 | 0x30 => Code::Tab, 110 | 0x31 => Code::Space, 111 | 0x32 => Code::Backquote, 112 | 0x33 => Code::Backspace, 113 | 0x34 => Code::NumpadEnter, 114 | 0x35 => Code::Escape, 115 | 0x36 => Code::MetaRight, 116 | 0x37 => Code::MetaLeft, 117 | 0x38 => Code::ShiftLeft, 118 | 0x39 => Code::CapsLock, 119 | // Note: in the linked source doc, this is "OSLeft" 120 | 0x3a => Code::AltLeft, 121 | 0x3b => Code::ControlLeft, 122 | 0x3c => Code::ShiftRight, 123 | // Note: in the linked source doc, this is "OSRight" 124 | 0x3d => Code::AltRight, 125 | 0x3e => Code::ControlRight, 126 | 0x3f => Code::Fn, // No events fired 127 | //0x40 => Code::F17, 128 | 0x41 => Code::NumpadDecimal, 129 | 0x43 => Code::NumpadMultiply, 130 | 0x45 => Code::NumpadAdd, 131 | 0x47 => Code::NumLock, 132 | 0x48 => Code::AudioVolumeUp, 133 | 0x49 => Code::AudioVolumeDown, 134 | 0x4a => Code::AudioVolumeMute, 135 | 0x4b => Code::NumpadDivide, 136 | 0x4c => Code::NumpadEnter, 137 | 0x4e => Code::NumpadSubtract, 138 | //0x4f => Code::F18, 139 | //0x50 => Code::F19, 140 | 0x51 => Code::NumpadEqual, 141 | 0x52 => Code::Numpad0, 142 | 0x53 => Code::Numpad1, 143 | 0x54 => Code::Numpad2, 144 | 0x55 => Code::Numpad3, 145 | 0x56 => Code::Numpad4, 146 | 0x57 => Code::Numpad5, 147 | 0x58 => Code::Numpad6, 148 | 0x59 => Code::Numpad7, 149 | //0x5a => Code::F20, 150 | 0x5b => Code::Numpad8, 151 | 0x5c => Code::Numpad9, 152 | 0x5d => Code::IntlYen, 153 | 0x5e => Code::IntlRo, 154 | 0x5f => Code::NumpadComma, 155 | 0x60 => Code::F5, 156 | 0x61 => Code::F6, 157 | 0x62 => Code::F7, 158 | 0x63 => Code::F3, 159 | 0x64 => Code::F8, 160 | 0x65 => Code::F9, 161 | 0x66 => Code::Lang2, 162 | 0x67 => Code::F11, 163 | 0x68 => Code::Lang1, 164 | // Note: this is listed as F13, but in testing with a standard 165 | // USB kb, this the code produced by PrtSc. 166 | 0x69 => Code::PrintScreen, 167 | //0x6a => Code::F16, 168 | //0x6b => Code::F14, 169 | 0x6d => Code::F10, 170 | 0x6e => Code::ContextMenu, 171 | 0x6f => Code::F12, 172 | //0x71 => Code::F15, 173 | 0x72 => Code::Help, 174 | 0x73 => Code::Home, 175 | 0x74 => Code::PageUp, 176 | 0x75 => Code::Delete, 177 | 0x76 => Code::F4, 178 | 0x77 => Code::End, 179 | 0x78 => Code::F2, 180 | 0x79 => Code::PageDown, 181 | 0x7a => Code::F1, 182 | 0x7b => Code::ArrowLeft, 183 | 0x7c => Code::ArrowRight, 184 | 0x7d => Code::ArrowDown, 185 | 0x7e => Code::ArrowUp, 186 | _ => Code::Unidentified, 187 | } 188 | } 189 | 190 | /// Convert code to key. 191 | /// 192 | /// On macOS, for non-printable keys, the keyCode we get from the event serves is 193 | /// really more of a key than a physical scan code. 194 | /// 195 | /// When this function returns None, the code can be considered printable. 196 | /// 197 | /// The logic for this function is derived from KEY_MAP_COCOA bindings in 198 | /// NativeKeyToDOMKeyName.h. 199 | fn code_to_key(code: Code) -> Option { 200 | Some(match code { 201 | Code::Escape => Key::Escape, 202 | Code::ShiftLeft | Code::ShiftRight => Key::Shift, 203 | Code::AltLeft | Code::AltRight => Key::Alt, 204 | Code::MetaLeft | Code::MetaRight => Key::Meta, 205 | Code::ControlLeft | Code::ControlRight => Key::Control, 206 | Code::CapsLock => Key::CapsLock, 207 | // kVK_ANSI_KeypadClear 208 | Code::NumLock => Key::Clear, 209 | Code::Fn => Key::Fn, 210 | Code::F1 => Key::F1, 211 | Code::F2 => Key::F2, 212 | Code::F3 => Key::F3, 213 | Code::F4 => Key::F4, 214 | Code::F5 => Key::F5, 215 | Code::F6 => Key::F6, 216 | Code::F7 => Key::F7, 217 | Code::F8 => Key::F8, 218 | Code::F9 => Key::F9, 219 | Code::F10 => Key::F10, 220 | Code::F11 => Key::F11, 221 | Code::F12 => Key::F12, 222 | Code::Pause => Key::Pause, 223 | Code::ScrollLock => Key::ScrollLock, 224 | Code::PrintScreen => Key::PrintScreen, 225 | Code::Insert => Key::Insert, 226 | Code::Delete => Key::Delete, 227 | Code::Tab => Key::Tab, 228 | Code::Backspace => Key::Backspace, 229 | Code::ContextMenu => Key::ContextMenu, 230 | // kVK_JIS_Kana 231 | Code::Lang1 => Key::KanjiMode, 232 | // kVK_JIS_Eisu 233 | Code::Lang2 => Key::Eisu, 234 | Code::Home => Key::Home, 235 | Code::End => Key::End, 236 | Code::PageUp => Key::PageUp, 237 | Code::PageDown => Key::PageDown, 238 | Code::ArrowLeft => Key::ArrowLeft, 239 | Code::ArrowRight => Key::ArrowRight, 240 | Code::ArrowUp => Key::ArrowUp, 241 | Code::ArrowDown => Key::ArrowDown, 242 | Code::Enter => Key::Enter, 243 | Code::NumpadEnter => Key::Enter, 244 | Code::Help => Key::Help, 245 | _ => return None, 246 | }) 247 | } 248 | 249 | fn is_valid_key(s: &str) -> bool { 250 | match s.chars().next() { 251 | None => false, 252 | Some(c) => c >= ' ' && c != '\x7f' && !('\u{e000}'..'\u{f900}').contains(&c), 253 | } 254 | } 255 | 256 | fn is_modifier_code(code: Code) -> bool { 257 | matches!( 258 | code, 259 | Code::ShiftLeft 260 | | Code::ShiftRight 261 | | Code::AltLeft 262 | | Code::AltRight 263 | | Code::ControlLeft 264 | | Code::ControlRight 265 | | Code::MetaLeft 266 | | Code::MetaRight 267 | | Code::CapsLock 268 | | Code::Help 269 | ) 270 | } 271 | 272 | impl KeyboardState { 273 | pub(crate) fn new() -> KeyboardState { 274 | let last_mods = Cell::new(NSEventModifierFlags::empty()); 275 | KeyboardState { last_mods } 276 | } 277 | 278 | pub(crate) fn last_mods(&self) -> NSEventModifierFlags { 279 | self.last_mods.get() 280 | } 281 | 282 | pub(crate) fn process_native_event(&self, event: id) -> Option { 283 | unsafe { 284 | let event_type = event.eventType(); 285 | let key_code = event.keyCode(); 286 | let code = key_code_to_code(key_code); 287 | let location = code_to_location(code); 288 | let raw_mods = event.modifierFlags(); 289 | let modifiers = make_modifiers(raw_mods); 290 | let state = match event_type { 291 | NSEventType::NSKeyDown => KeyState::Down, 292 | NSEventType::NSKeyUp => KeyState::Up, 293 | NSEventType::NSFlagsChanged => { 294 | // We use `bits` here because we want to distinguish the 295 | // device dependent bits (when both left and right keys 296 | // may be pressed, for example). 297 | let any_down = raw_mods.bits() & !self.last_mods.get().bits(); 298 | self.last_mods.set(raw_mods); 299 | if is_modifier_code(code) { 300 | if any_down == 0 { 301 | KeyState::Up 302 | } else { 303 | KeyState::Down 304 | } 305 | } else { 306 | // HandleFlagsChanged has some logic for this; it might 307 | // happen when an app is deactivated by Command-Tab. In 308 | // that case, the best thing to do is synthesize the event 309 | // from the modifiers. But a challenge there is that we 310 | // might get multiple events. 311 | return None; 312 | } 313 | } 314 | _ => unreachable!(), 315 | }; 316 | let is_composing = false; 317 | let repeat: bool = event_type == NSEventType::NSKeyDown && msg_send![event, isARepeat]; 318 | let key = if let Some(key) = code_to_key(code) { 319 | key 320 | } else { 321 | let characters = from_nsstring(event.characters()); 322 | if is_valid_key(&characters) { 323 | Key::Character(characters) 324 | } else { 325 | let chars_ignoring = from_nsstring(event.charactersIgnoringModifiers()); 326 | if is_valid_key(&chars_ignoring) { 327 | Key::Character(chars_ignoring) 328 | } else { 329 | // There may be more heroic things we can do here. 330 | Key::Unidentified 331 | } 332 | } 333 | }; 334 | let event = 335 | KeyboardEvent { code, key, location, modifiers, state, is_composing, repeat }; 336 | Some(event) 337 | } 338 | } 339 | } 340 | 341 | const MODIFIER_MAP: &[(NSEventModifierFlags, Modifiers)] = &[ 342 | (NSEventModifierFlags::NSShiftKeyMask, Modifiers::SHIFT), 343 | (NSEventModifierFlags::NSAlternateKeyMask, Modifiers::ALT), 344 | (NSEventModifierFlags::NSControlKeyMask, Modifiers::CONTROL), 345 | (NSEventModifierFlags::NSCommandKeyMask, Modifiers::META), 346 | (NSEventModifierFlags::NSAlphaShiftKeyMask, Modifiers::CAPS_LOCK), 347 | ]; 348 | 349 | pub(crate) fn make_modifiers(raw: NSEventModifierFlags) -> Modifiers { 350 | let mut modifiers = Modifiers::empty(); 351 | for &(flags, mods) in MODIFIER_MAP { 352 | if raw.contains(flags) { 353 | modifiers |= mods; 354 | } 355 | } 356 | modifiers 357 | } 358 | -------------------------------------------------------------------------------- /src/macos/mod.rs: -------------------------------------------------------------------------------- 1 | // This is required because the objc crate is causing a lot of warnings: https://github.com/SSheldon/rust-objc/issues/125 2 | // Eventually we should migrate to the objc2 crate and remove this. 3 | #![allow(unexpected_cfgs)] 4 | 5 | mod keyboard; 6 | mod view; 7 | mod window; 8 | 9 | pub use window::*; 10 | 11 | #[allow(non_upper_case_globals)] 12 | mod consts { 13 | use cocoa::foundation::NSUInteger; 14 | 15 | pub const NSDragOperationNone: NSUInteger = 0; 16 | pub const NSDragOperationCopy: NSUInteger = 1; 17 | pub const NSDragOperationLink: NSUInteger = 2; 18 | pub const NSDragOperationGeneric: NSUInteger = 4; 19 | pub const NSDragOperationMove: NSUInteger = 16; 20 | } 21 | use consts::*; 22 | -------------------------------------------------------------------------------- /src/macos/view.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use cocoa::appkit::{NSEvent, NSFilenamesPboardType, NSView, NSWindow}; 4 | use cocoa::base::{id, nil, BOOL, NO, YES}; 5 | use cocoa::foundation::{NSArray, NSPoint, NSRect, NSSize, NSUInteger}; 6 | 7 | use objc::{ 8 | class, 9 | declare::ClassDecl, 10 | msg_send, 11 | runtime::{Class, Object, Sel}, 12 | sel, sel_impl, 13 | }; 14 | use uuid::Uuid; 15 | 16 | use crate::MouseEvent::{ButtonPressed, ButtonReleased}; 17 | use crate::{ 18 | DropData, DropEffect, Event, EventStatus, MouseButton, MouseEvent, Point, ScrollDelta, Size, 19 | WindowEvent, WindowInfo, WindowOpenOptions, 20 | }; 21 | 22 | use super::keyboard::{from_nsstring, make_modifiers}; 23 | use super::window::WindowState; 24 | use super::{ 25 | NSDragOperationCopy, NSDragOperationGeneric, NSDragOperationLink, NSDragOperationMove, 26 | NSDragOperationNone, 27 | }; 28 | 29 | /// Name of the field used to store the `WindowState` pointer. 30 | pub(super) const BASEVIEW_STATE_IVAR: &str = "baseview_state"; 31 | 32 | #[link(name = "AppKit", kind = "framework")] 33 | extern "C" { 34 | static NSWindowDidBecomeKeyNotification: id; 35 | static NSWindowDidResignKeyNotification: id; 36 | } 37 | 38 | macro_rules! add_simple_mouse_class_method { 39 | ($class:ident, $sel:ident, $event:expr) => { 40 | #[allow(non_snake_case)] 41 | extern "C" fn $sel(this: &Object, _: Sel, _: id){ 42 | let state = unsafe { WindowState::from_view(this) }; 43 | 44 | state.trigger_event(Event::Mouse($event)); 45 | } 46 | 47 | $class.add_method( 48 | sel!($sel:), 49 | $sel as extern "C" fn(&Object, Sel, id), 50 | ); 51 | }; 52 | } 53 | 54 | /// Similar to [add_simple_mouse_class_method!], but this creates its own event object for the 55 | /// press/release event and adds the active modifier keys to that event. 56 | macro_rules! add_mouse_button_class_method { 57 | ($class:ident, $sel:ident, $event_ty:ident, $button:expr) => { 58 | #[allow(non_snake_case)] 59 | extern "C" fn $sel(this: &Object, _: Sel, event: id){ 60 | let state = unsafe { WindowState::from_view(this) }; 61 | 62 | let modifiers = unsafe { NSEvent::modifierFlags(event) }; 63 | 64 | state.trigger_event(Event::Mouse($event_ty { 65 | button: $button, 66 | modifiers: make_modifiers(modifiers), 67 | })); 68 | } 69 | 70 | $class.add_method( 71 | sel!($sel:), 72 | $sel as extern "C" fn(&Object, Sel, id), 73 | ); 74 | }; 75 | } 76 | 77 | macro_rules! add_simple_keyboard_class_method { 78 | ($class:ident, $sel:ident) => { 79 | #[allow(non_snake_case)] 80 | extern "C" fn $sel(this: &Object, _: Sel, event: id){ 81 | let state = unsafe { WindowState::from_view(this) }; 82 | 83 | if let Some(key_event) = state.process_native_key_event(event){ 84 | let status = state.trigger_event(Event::Keyboard(key_event)); 85 | 86 | if let EventStatus::Ignored = status { 87 | unsafe { 88 | let superclass = msg_send![this, superclass]; 89 | 90 | let () = msg_send![super(this, superclass), $sel:event]; 91 | } 92 | } 93 | } 94 | } 95 | 96 | $class.add_method( 97 | sel!($sel:), 98 | $sel as extern "C" fn(&Object, Sel, id), 99 | ); 100 | }; 101 | } 102 | 103 | unsafe fn register_notification(observer: id, notification_name: id, object: id) { 104 | let notification_center: id = msg_send![class!(NSNotificationCenter), defaultCenter]; 105 | 106 | let _: () = msg_send![ 107 | notification_center, 108 | addObserver:observer 109 | selector:sel!(handleNotification:) 110 | name:notification_name 111 | object:object 112 | ]; 113 | } 114 | 115 | pub(super) unsafe fn create_view(window_options: &WindowOpenOptions) -> id { 116 | let class = create_view_class(); 117 | 118 | let view: id = msg_send![class, alloc]; 119 | 120 | let size = window_options.size; 121 | 122 | view.initWithFrame_(NSRect::new(NSPoint::new(0., 0.), NSSize::new(size.width, size.height))); 123 | 124 | register_notification(view, NSWindowDidBecomeKeyNotification, nil); 125 | register_notification(view, NSWindowDidResignKeyNotification, nil); 126 | 127 | let _: id = msg_send![ 128 | view, 129 | registerForDraggedTypes: NSArray::arrayWithObjects(nil, &[NSFilenamesPboardType]) 130 | ]; 131 | 132 | view 133 | } 134 | 135 | unsafe fn create_view_class() -> &'static Class { 136 | // Use unique class names so that there are no conflicts between different 137 | // instances. The class is deleted when the view is released. Previously, 138 | // the class was stored in a OnceCell after creation. This way, we didn't 139 | // have to recreate it each time a view was opened, but now we don't leave 140 | // any class definitions lying around when the plugin is closed. 141 | let class_name = format!("BaseviewNSView_{}", Uuid::new_v4().to_simple()); 142 | let mut class = ClassDecl::new(&class_name, class!(NSView)).unwrap(); 143 | 144 | class.add_method( 145 | sel!(acceptsFirstResponder), 146 | property_yes as extern "C" fn(&Object, Sel) -> BOOL, 147 | ); 148 | class.add_method( 149 | sel!(becomeFirstResponder), 150 | become_first_responder as extern "C" fn(&Object, Sel) -> BOOL, 151 | ); 152 | class.add_method( 153 | sel!(resignFirstResponder), 154 | resign_first_responder as extern "C" fn(&Object, Sel) -> BOOL, 155 | ); 156 | class.add_method(sel!(isFlipped), property_yes as extern "C" fn(&Object, Sel) -> BOOL); 157 | class.add_method( 158 | sel!(preservesContentInLiveResize), 159 | property_no as extern "C" fn(&Object, Sel) -> BOOL, 160 | ); 161 | class.add_method( 162 | sel!(acceptsFirstMouse:), 163 | accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL, 164 | ); 165 | 166 | class.add_method( 167 | sel!(windowShouldClose:), 168 | window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, 169 | ); 170 | class.add_method(sel!(dealloc), dealloc as extern "C" fn(&mut Object, Sel)); 171 | class.add_method( 172 | sel!(viewWillMoveToWindow:), 173 | view_will_move_to_window as extern "C" fn(&Object, Sel, id), 174 | ); 175 | class.add_method( 176 | sel!(updateTrackingAreas:), 177 | update_tracking_areas as extern "C" fn(&Object, Sel, id), 178 | ); 179 | 180 | class.add_method(sel!(mouseMoved:), mouse_moved as extern "C" fn(&Object, Sel, id)); 181 | class.add_method(sel!(mouseDragged:), mouse_moved as extern "C" fn(&Object, Sel, id)); 182 | class.add_method(sel!(rightMouseDragged:), mouse_moved as extern "C" fn(&Object, Sel, id)); 183 | class.add_method(sel!(otherMouseDragged:), mouse_moved as extern "C" fn(&Object, Sel, id)); 184 | 185 | class.add_method(sel!(scrollWheel:), scroll_wheel as extern "C" fn(&Object, Sel, id)); 186 | 187 | class.add_method( 188 | sel!(viewDidChangeBackingProperties:), 189 | view_did_change_backing_properties as extern "C" fn(&Object, Sel, id), 190 | ); 191 | 192 | class.add_method( 193 | sel!(draggingEntered:), 194 | dragging_entered as extern "C" fn(&Object, Sel, id) -> NSUInteger, 195 | ); 196 | class.add_method( 197 | sel!(prepareForDragOperation:), 198 | prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, 199 | ); 200 | class.add_method( 201 | sel!(performDragOperation:), 202 | perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, 203 | ); 204 | class.add_method( 205 | sel!(draggingUpdated:), 206 | dragging_updated as extern "C" fn(&Object, Sel, id) -> NSUInteger, 207 | ); 208 | class.add_method(sel!(draggingExited:), dragging_exited as extern "C" fn(&Object, Sel, id)); 209 | class.add_method( 210 | sel!(handleNotification:), 211 | handle_notification as extern "C" fn(&Object, Sel, id), 212 | ); 213 | 214 | add_mouse_button_class_method!(class, mouseDown, ButtonPressed, MouseButton::Left); 215 | add_mouse_button_class_method!(class, mouseUp, ButtonReleased, MouseButton::Left); 216 | add_mouse_button_class_method!(class, rightMouseDown, ButtonPressed, MouseButton::Right); 217 | add_mouse_button_class_method!(class, rightMouseUp, ButtonReleased, MouseButton::Right); 218 | add_mouse_button_class_method!(class, otherMouseDown, ButtonPressed, MouseButton::Middle); 219 | add_mouse_button_class_method!(class, otherMouseUp, ButtonReleased, MouseButton::Middle); 220 | add_simple_mouse_class_method!(class, mouseEntered, MouseEvent::CursorEntered); 221 | add_simple_mouse_class_method!(class, mouseExited, MouseEvent::CursorLeft); 222 | 223 | add_simple_keyboard_class_method!(class, keyDown); 224 | add_simple_keyboard_class_method!(class, keyUp); 225 | add_simple_keyboard_class_method!(class, flagsChanged); 226 | 227 | class.add_ivar::<*mut c_void>(BASEVIEW_STATE_IVAR); 228 | 229 | class.register() 230 | } 231 | 232 | extern "C" fn property_yes(_this: &Object, _sel: Sel) -> BOOL { 233 | YES 234 | } 235 | 236 | extern "C" fn property_no(_this: &Object, _sel: Sel) -> BOOL { 237 | NO 238 | } 239 | 240 | extern "C" fn accepts_first_mouse(_this: &Object, _sel: Sel, _event: id) -> BOOL { 241 | YES 242 | } 243 | 244 | extern "C" fn become_first_responder(this: &Object, _sel: Sel) -> BOOL { 245 | let state = unsafe { WindowState::from_view(this) }; 246 | let is_key_window = unsafe { 247 | let window: id = msg_send![this, window]; 248 | if window != nil { 249 | let is_key_window: BOOL = msg_send![window, isKeyWindow]; 250 | is_key_window == YES 251 | } else { 252 | false 253 | } 254 | }; 255 | if is_key_window { 256 | state.trigger_deferrable_event(Event::Window(WindowEvent::Focused)); 257 | } 258 | YES 259 | } 260 | 261 | extern "C" fn resign_first_responder(this: &Object, _sel: Sel) -> BOOL { 262 | let state = unsafe { WindowState::from_view(this) }; 263 | state.trigger_deferrable_event(Event::Window(WindowEvent::Unfocused)); 264 | YES 265 | } 266 | 267 | extern "C" fn window_should_close(this: &Object, _: Sel, _sender: id) -> BOOL { 268 | let state = unsafe { WindowState::from_view(this) }; 269 | 270 | state.trigger_event(Event::Window(WindowEvent::WillClose)); 271 | 272 | state.window_inner.close(); 273 | 274 | NO 275 | } 276 | 277 | extern "C" fn dealloc(this: &mut Object, _sel: Sel) { 278 | unsafe { 279 | let class = msg_send![this, class]; 280 | 281 | let superclass = msg_send![this, superclass]; 282 | let () = msg_send![super(this, superclass), dealloc]; 283 | 284 | // Delete class 285 | ::objc::runtime::objc_disposeClassPair(class); 286 | } 287 | } 288 | 289 | extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel, _: id) { 290 | unsafe { 291 | let ns_window: *mut Object = msg_send![this, window]; 292 | 293 | let scale_factor: f64 = 294 | if ns_window.is_null() { 1.0 } else { NSWindow::backingScaleFactor(ns_window) }; 295 | 296 | let state = WindowState::from_view(this); 297 | 298 | let bounds: NSRect = msg_send![this, bounds]; 299 | 300 | let new_window_info = WindowInfo::from_logical_size( 301 | Size::new(bounds.size.width, bounds.size.height), 302 | scale_factor, 303 | ); 304 | 305 | let window_info = state.window_info.get(); 306 | 307 | // Only send the event when the window's size has actually changed to be in line with the 308 | // other platform implementations 309 | if new_window_info.physical_size() != window_info.physical_size() { 310 | state.window_info.set(new_window_info); 311 | state.trigger_event(Event::Window(WindowEvent::Resized(new_window_info))); 312 | } 313 | } 314 | } 315 | 316 | /// Init/reinit tracking area 317 | /// 318 | /// Info: 319 | /// https://developer.apple.com/documentation/appkit/nstrackingarea 320 | /// https://developer.apple.com/documentation/appkit/nstrackingarea/options 321 | /// https://developer.apple.com/documentation/appkit/nstrackingareaoptions 322 | unsafe fn reinit_tracking_area(this: &Object, tracking_area: *mut Object) { 323 | let options: usize = { 324 | let mouse_entered_and_exited = 0x01; 325 | let tracking_mouse_moved = 0x02; 326 | let tracking_cursor_update = 0x04; 327 | let tracking_active_in_active_app = 0x40; 328 | let tracking_in_visible_rect = 0x200; 329 | let tracking_enabled_during_mouse_drag = 0x400; 330 | 331 | mouse_entered_and_exited 332 | | tracking_mouse_moved 333 | | tracking_cursor_update 334 | | tracking_active_in_active_app 335 | | tracking_in_visible_rect 336 | | tracking_enabled_during_mouse_drag 337 | }; 338 | 339 | let bounds: NSRect = msg_send![this, bounds]; 340 | 341 | *tracking_area = msg_send![tracking_area, 342 | initWithRect:bounds 343 | options:options 344 | owner:this 345 | userInfo:nil 346 | ]; 347 | } 348 | 349 | extern "C" fn view_will_move_to_window(this: &Object, _self: Sel, new_window: id) { 350 | unsafe { 351 | let tracking_areas: *mut Object = msg_send![this, trackingAreas]; 352 | let tracking_area_count = NSArray::count(tracking_areas); 353 | 354 | if new_window == nil { 355 | if tracking_area_count != 0 { 356 | let tracking_area = NSArray::objectAtIndex(tracking_areas, 0); 357 | 358 | let _: () = msg_send![this, removeTrackingArea: tracking_area]; 359 | let _: () = msg_send![tracking_area, release]; 360 | } 361 | } else { 362 | if tracking_area_count == 0 { 363 | let class = Class::get("NSTrackingArea").unwrap(); 364 | 365 | let tracking_area: *mut Object = msg_send![class, alloc]; 366 | 367 | reinit_tracking_area(this, tracking_area); 368 | 369 | let _: () = msg_send![this, addTrackingArea: tracking_area]; 370 | } 371 | 372 | let _: () = msg_send![new_window, setAcceptsMouseMovedEvents: YES]; 373 | let _: () = msg_send![new_window, makeFirstResponder: this]; 374 | } 375 | } 376 | 377 | unsafe { 378 | let superclass = msg_send![this, superclass]; 379 | 380 | let () = msg_send![super(this, superclass), viewWillMoveToWindow: new_window]; 381 | } 382 | } 383 | 384 | extern "C" fn update_tracking_areas(this: &Object, _self: Sel, _: id) { 385 | unsafe { 386 | let tracking_areas: *mut Object = msg_send![this, trackingAreas]; 387 | let tracking_area = NSArray::objectAtIndex(tracking_areas, 0); 388 | 389 | reinit_tracking_area(this, tracking_area); 390 | } 391 | } 392 | 393 | extern "C" fn mouse_moved(this: &Object, _sel: Sel, event: id) { 394 | let state = unsafe { WindowState::from_view(this) }; 395 | 396 | let point: NSPoint = unsafe { 397 | let point = NSEvent::locationInWindow(event); 398 | 399 | msg_send![this, convertPoint:point fromView:nil] 400 | }; 401 | let modifiers = unsafe { NSEvent::modifierFlags(event) }; 402 | 403 | let position = Point { x: point.x, y: point.y }; 404 | 405 | state.trigger_event(Event::Mouse(MouseEvent::CursorMoved { 406 | position, 407 | modifiers: make_modifiers(modifiers), 408 | })); 409 | } 410 | 411 | extern "C" fn scroll_wheel(this: &Object, _: Sel, event: id) { 412 | let state = unsafe { WindowState::from_view(this) }; 413 | 414 | let delta = unsafe { 415 | let x = NSEvent::scrollingDeltaX(event) as f32; 416 | let y = NSEvent::scrollingDeltaY(event) as f32; 417 | 418 | if NSEvent::hasPreciseScrollingDeltas(event) != NO { 419 | ScrollDelta::Pixels { x, y } 420 | } else { 421 | ScrollDelta::Lines { x, y } 422 | } 423 | }; 424 | 425 | let modifiers = unsafe { NSEvent::modifierFlags(event) }; 426 | 427 | state.trigger_event(Event::Mouse(MouseEvent::WheelScrolled { 428 | delta, 429 | modifiers: make_modifiers(modifiers), 430 | })); 431 | } 432 | 433 | fn get_drag_position(sender: id) -> Point { 434 | let point: NSPoint = unsafe { msg_send![sender, draggingLocation] }; 435 | Point::new(point.x, point.y) 436 | } 437 | 438 | fn get_drop_data(sender: id) -> DropData { 439 | if sender == nil { 440 | return DropData::None; 441 | } 442 | 443 | unsafe { 444 | let pasteboard: id = msg_send![sender, draggingPasteboard]; 445 | let file_list: id = msg_send![pasteboard, propertyListForType: NSFilenamesPboardType]; 446 | 447 | if file_list == nil { 448 | return DropData::None; 449 | } 450 | 451 | let mut files = vec![]; 452 | for i in 0..NSArray::count(file_list) { 453 | let data = NSArray::objectAtIndex(file_list, i); 454 | files.push(from_nsstring(data).into()); 455 | } 456 | 457 | DropData::Files(files) 458 | } 459 | } 460 | 461 | fn on_event(window_state: &WindowState, event: MouseEvent) -> NSUInteger { 462 | let event_status = window_state.trigger_event(Event::Mouse(event)); 463 | match event_status { 464 | EventStatus::AcceptDrop(DropEffect::Copy) => NSDragOperationCopy, 465 | EventStatus::AcceptDrop(DropEffect::Move) => NSDragOperationMove, 466 | EventStatus::AcceptDrop(DropEffect::Link) => NSDragOperationLink, 467 | EventStatus::AcceptDrop(DropEffect::Scroll) => NSDragOperationGeneric, 468 | _ => NSDragOperationNone, 469 | } 470 | } 471 | 472 | extern "C" fn dragging_entered(this: &Object, _sel: Sel, sender: id) -> NSUInteger { 473 | let state = unsafe { WindowState::from_view(this) }; 474 | let modifiers = state.keyboard_state().last_mods(); 475 | let drop_data = get_drop_data(sender); 476 | 477 | let event = MouseEvent::DragEntered { 478 | position: get_drag_position(sender), 479 | modifiers: make_modifiers(modifiers), 480 | data: drop_data, 481 | }; 482 | 483 | on_event(&state, event) 484 | } 485 | 486 | extern "C" fn dragging_updated(this: &Object, _sel: Sel, sender: id) -> NSUInteger { 487 | let state = unsafe { WindowState::from_view(this) }; 488 | let modifiers = state.keyboard_state().last_mods(); 489 | let drop_data = get_drop_data(sender); 490 | 491 | let event = MouseEvent::DragMoved { 492 | position: get_drag_position(sender), 493 | modifiers: make_modifiers(modifiers), 494 | data: drop_data, 495 | }; 496 | 497 | on_event(&state, event) 498 | } 499 | 500 | extern "C" fn prepare_for_drag_operation(_this: &Object, _sel: Sel, _sender: id) -> BOOL { 501 | // Always accept drag operation if we get this far 502 | // This function won't be called unless dragging_entered/updated 503 | // has returned an acceptable operation 504 | YES 505 | } 506 | 507 | extern "C" fn perform_drag_operation(this: &Object, _sel: Sel, sender: id) -> BOOL { 508 | let state = unsafe { WindowState::from_view(this) }; 509 | let modifiers = state.keyboard_state().last_mods(); 510 | let drop_data = get_drop_data(sender); 511 | 512 | let event = MouseEvent::DragDropped { 513 | position: get_drag_position(sender), 514 | modifiers: make_modifiers(modifiers), 515 | data: drop_data, 516 | }; 517 | 518 | let event_status = state.trigger_event(Event::Mouse(event)); 519 | match event_status { 520 | EventStatus::AcceptDrop(_) => YES, 521 | _ => NO, 522 | } 523 | } 524 | 525 | extern "C" fn dragging_exited(this: &Object, _sel: Sel, _sender: id) { 526 | let state = unsafe { WindowState::from_view(this) }; 527 | 528 | on_event(&state, MouseEvent::DragLeft); 529 | } 530 | 531 | extern "C" fn handle_notification(this: &Object, _cmd: Sel, notification: id) { 532 | unsafe { 533 | let state = WindowState::from_view(this); 534 | 535 | // The subject of the notication, in this case an NSWindow object. 536 | let notification_object: id = msg_send![notification, object]; 537 | 538 | // The NSWindow object associated with our NSView. 539 | let window: id = msg_send![this, window]; 540 | 541 | let first_responder: id = msg_send![window, firstResponder]; 542 | 543 | // Only trigger focus events if the NSWindow that's being notified about is our window, 544 | // and if the window's first responder is our NSView. 545 | // If the first responder isn't our NSView, the focus events will instead be triggered 546 | // by the becomeFirstResponder and resignFirstResponder methods on the NSView itself. 547 | if notification_object == window && first_responder == this as *const Object as id { 548 | let is_key_window: BOOL = msg_send![window, isKeyWindow]; 549 | state.trigger_event(Event::Window(if is_key_window == YES { 550 | WindowEvent::Focused 551 | } else { 552 | WindowEvent::Unfocused 553 | })); 554 | } 555 | } 556 | } 557 | -------------------------------------------------------------------------------- /src/macos/window.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{Cell, RefCell}; 2 | use std::collections::VecDeque; 3 | use std::ffi::c_void; 4 | use std::ptr; 5 | use std::rc::Rc; 6 | 7 | use cocoa::appkit::{ 8 | NSApp, NSApplication, NSApplicationActivationPolicyRegular, NSBackingStoreBuffered, 9 | NSPasteboard, NSView, NSWindow, NSWindowStyleMask, 10 | }; 11 | use cocoa::base::{id, nil, BOOL, NO, YES}; 12 | use cocoa::foundation::{NSAutoreleasePool, NSPoint, NSRect, NSSize, NSString}; 13 | use core_foundation::runloop::{ 14 | CFRunLoop, CFRunLoopTimer, CFRunLoopTimerContext, __CFRunLoopTimer, kCFRunLoopDefaultMode, 15 | }; 16 | use keyboard_types::KeyboardEvent; 17 | use objc::class; 18 | use objc::{msg_send, runtime::Object, sel, sel_impl}; 19 | use raw_window_handle::{ 20 | AppKitDisplayHandle, AppKitWindowHandle, HasRawDisplayHandle, HasRawWindowHandle, 21 | RawDisplayHandle, RawWindowHandle, 22 | }; 23 | 24 | use crate::{ 25 | Event, EventStatus, MouseCursor, Size, WindowHandler, WindowInfo, WindowOpenOptions, 26 | WindowScalePolicy, 27 | }; 28 | 29 | use super::keyboard::KeyboardState; 30 | use super::view::{create_view, BASEVIEW_STATE_IVAR}; 31 | 32 | #[cfg(feature = "opengl")] 33 | use crate::gl::{GlConfig, GlContext}; 34 | 35 | pub struct WindowHandle { 36 | state: Rc, 37 | } 38 | 39 | impl WindowHandle { 40 | pub fn close(&mut self) { 41 | self.state.window_inner.close(); 42 | } 43 | 44 | pub fn is_open(&self) -> bool { 45 | self.state.window_inner.open.get() 46 | } 47 | } 48 | 49 | unsafe impl HasRawWindowHandle for WindowHandle { 50 | fn raw_window_handle(&self) -> RawWindowHandle { 51 | self.state.window_inner.raw_window_handle() 52 | } 53 | } 54 | 55 | pub(super) struct WindowInner { 56 | open: Cell, 57 | 58 | /// Only set if we created the parent window, i.e. we are running in 59 | /// parentless mode 60 | ns_app: Cell>, 61 | /// Only set if we created the parent window, i.e. we are running in 62 | /// parentless mode 63 | ns_window: Cell>, 64 | /// Our subclassed NSView 65 | ns_view: id, 66 | 67 | #[cfg(feature = "opengl")] 68 | gl_context: Option, 69 | } 70 | 71 | impl WindowInner { 72 | pub(super) fn close(&self) { 73 | if self.open.get() { 74 | self.open.set(false); 75 | unsafe { 76 | // Take back ownership of the NSView's Rc 77 | let state_ptr: *const c_void = *(*self.ns_view).get_ivar(BASEVIEW_STATE_IVAR); 78 | let window_state = Rc::from_raw(state_ptr as *mut WindowState); 79 | 80 | // Cancel the frame timer 81 | if let Some(frame_timer) = window_state.frame_timer.take() { 82 | CFRunLoop::get_current().remove_timer(&frame_timer, kCFRunLoopDefaultMode); 83 | } 84 | 85 | // Deregister NSView from NotificationCenter. 86 | let notification_center: id = 87 | msg_send![class!(NSNotificationCenter), defaultCenter]; 88 | let () = msg_send![notification_center, removeObserver:self.ns_view]; 89 | 90 | drop(window_state); 91 | 92 | // Close the window if in non-parented mode 93 | if let Some(ns_window) = self.ns_window.take() { 94 | ns_window.close(); 95 | } 96 | 97 | // Ensure that the NSView is detached from the parent window 98 | self.ns_view.removeFromSuperview(); 99 | let () = msg_send![self.ns_view as id, release]; 100 | 101 | // If in non-parented mode, we want to also quit the app altogether 102 | let app = self.ns_app.take(); 103 | if let Some(app) = app { 104 | app.stop_(app); 105 | } 106 | } 107 | } 108 | } 109 | 110 | fn raw_window_handle(&self) -> RawWindowHandle { 111 | if self.open.get() { 112 | let ns_window = self.ns_window.get().unwrap_or(ptr::null_mut()) as *mut c_void; 113 | 114 | let mut handle = AppKitWindowHandle::empty(); 115 | handle.ns_window = ns_window; 116 | handle.ns_view = self.ns_view as *mut c_void; 117 | 118 | return RawWindowHandle::AppKit(handle); 119 | } 120 | 121 | RawWindowHandle::AppKit(AppKitWindowHandle::empty()) 122 | } 123 | } 124 | 125 | pub struct Window<'a> { 126 | inner: &'a WindowInner, 127 | } 128 | 129 | impl<'a> Window<'a> { 130 | pub fn open_parented(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle 131 | where 132 | P: HasRawWindowHandle, 133 | H: WindowHandler + 'static, 134 | B: FnOnce(&mut crate::Window) -> H, 135 | B: Send + 'static, 136 | { 137 | let pool = unsafe { NSAutoreleasePool::new(nil) }; 138 | 139 | let scaling = match options.scale { 140 | WindowScalePolicy::ScaleFactor(scale) => scale, 141 | WindowScalePolicy::SystemScaleFactor => 1.0, 142 | }; 143 | 144 | let window_info = WindowInfo::from_logical_size(options.size, scaling); 145 | 146 | let handle = if let RawWindowHandle::AppKit(handle) = parent.raw_window_handle() { 147 | handle 148 | } else { 149 | panic!("Not a macOS window"); 150 | }; 151 | 152 | let ns_view = unsafe { create_view(&options) }; 153 | 154 | let window_inner = WindowInner { 155 | open: Cell::new(true), 156 | ns_app: Cell::new(None), 157 | ns_window: Cell::new(None), 158 | ns_view, 159 | 160 | #[cfg(feature = "opengl")] 161 | gl_context: options 162 | .gl_config 163 | .map(|gl_config| Self::create_gl_context(None, ns_view, gl_config)), 164 | }; 165 | 166 | let window_handle = Self::init(window_inner, window_info, build); 167 | 168 | unsafe { 169 | let _: id = msg_send![handle.ns_view as *mut Object, addSubview: ns_view]; 170 | 171 | let () = msg_send![pool, drain]; 172 | } 173 | 174 | window_handle 175 | } 176 | 177 | pub fn open_blocking(options: WindowOpenOptions, build: B) 178 | where 179 | H: WindowHandler + 'static, 180 | B: FnOnce(&mut crate::Window) -> H, 181 | B: Send + 'static, 182 | { 183 | let pool = unsafe { NSAutoreleasePool::new(nil) }; 184 | 185 | // It seems prudent to run NSApp() here before doing other 186 | // work. It runs [NSApplication sharedApplication], which is 187 | // what is run at the very start of the Xcode-generated main 188 | // function of a cocoa app according to: 189 | // https://developer.apple.com/documentation/appkit/nsapplication 190 | let app = unsafe { NSApp() }; 191 | 192 | unsafe { 193 | app.setActivationPolicy_(NSApplicationActivationPolicyRegular); 194 | } 195 | 196 | let scaling = match options.scale { 197 | WindowScalePolicy::ScaleFactor(scale) => scale, 198 | WindowScalePolicy::SystemScaleFactor => 1.0, 199 | }; 200 | 201 | let window_info = WindowInfo::from_logical_size(options.size, scaling); 202 | 203 | let rect = NSRect::new( 204 | NSPoint::new(0.0, 0.0), 205 | NSSize::new(window_info.logical_size().width, window_info.logical_size().height), 206 | ); 207 | 208 | let ns_window = unsafe { 209 | let ns_window = NSWindow::alloc(nil).initWithContentRect_styleMask_backing_defer_( 210 | rect, 211 | NSWindowStyleMask::NSTitledWindowMask 212 | | NSWindowStyleMask::NSClosableWindowMask 213 | | NSWindowStyleMask::NSMiniaturizableWindowMask, 214 | NSBackingStoreBuffered, 215 | NO, 216 | ); 217 | ns_window.center(); 218 | 219 | let title = NSString::alloc(nil).init_str(&options.title).autorelease(); 220 | ns_window.setTitle_(title); 221 | 222 | ns_window.makeKeyAndOrderFront_(nil); 223 | 224 | ns_window 225 | }; 226 | 227 | let ns_view = unsafe { create_view(&options) }; 228 | 229 | let window_inner = WindowInner { 230 | open: Cell::new(true), 231 | ns_app: Cell::new(Some(app)), 232 | ns_window: Cell::new(Some(ns_window)), 233 | ns_view, 234 | 235 | #[cfg(feature = "opengl")] 236 | gl_context: options 237 | .gl_config 238 | .map(|gl_config| Self::create_gl_context(Some(ns_window), ns_view, gl_config)), 239 | }; 240 | 241 | let _ = Self::init(window_inner, window_info, build); 242 | 243 | unsafe { 244 | ns_window.setContentView_(ns_view); 245 | ns_window.setDelegate_(ns_view); 246 | 247 | let () = msg_send![pool, drain]; 248 | 249 | app.run(); 250 | } 251 | } 252 | 253 | fn init(window_inner: WindowInner, window_info: WindowInfo, build: B) -> WindowHandle 254 | where 255 | H: WindowHandler + 'static, 256 | B: FnOnce(&mut crate::Window) -> H, 257 | B: Send + 'static, 258 | { 259 | let mut window = crate::Window::new(Window { inner: &window_inner }); 260 | let window_handler = Box::new(build(&mut window)); 261 | 262 | let ns_view = window_inner.ns_view; 263 | 264 | let window_state = Rc::new(WindowState { 265 | window_inner, 266 | window_handler: RefCell::new(window_handler), 267 | keyboard_state: KeyboardState::new(), 268 | frame_timer: Cell::new(None), 269 | window_info: Cell::new(window_info), 270 | deferred_events: RefCell::default(), 271 | }); 272 | 273 | let window_state_ptr = Rc::into_raw(Rc::clone(&window_state)); 274 | 275 | unsafe { 276 | (*ns_view).set_ivar(BASEVIEW_STATE_IVAR, window_state_ptr as *const c_void); 277 | 278 | WindowState::setup_timer(window_state_ptr); 279 | } 280 | 281 | WindowHandle { state: window_state } 282 | } 283 | 284 | pub fn close(&mut self) { 285 | self.inner.close(); 286 | } 287 | 288 | pub fn has_focus(&mut self) -> bool { 289 | unsafe { 290 | let view = self.inner.ns_view.as_mut().unwrap(); 291 | let window: id = msg_send![view, window]; 292 | if window == nil { 293 | return false; 294 | }; 295 | let first_responder: id = msg_send![window, firstResponder]; 296 | let is_key_window: BOOL = msg_send![window, isKeyWindow]; 297 | let is_focused: BOOL = msg_send![view, isEqual: first_responder]; 298 | is_key_window == YES && is_focused == YES 299 | } 300 | } 301 | 302 | pub fn focus(&mut self) { 303 | unsafe { 304 | let view = self.inner.ns_view.as_mut().unwrap(); 305 | let window: id = msg_send![view, window]; 306 | if window != nil { 307 | msg_send![window, makeFirstResponder:view] 308 | } 309 | } 310 | } 311 | 312 | pub fn resize(&mut self, size: Size) { 313 | if self.inner.open.get() { 314 | // NOTE: macOS gives you a personal rave if you pass in fractional pixels here. Even 315 | // though the size is in fractional pixels. 316 | let size = NSSize::new(size.width.round(), size.height.round()); 317 | 318 | unsafe { NSView::setFrameSize(self.inner.ns_view, size) }; 319 | unsafe { 320 | let _: () = msg_send![self.inner.ns_view, setNeedsDisplay: YES]; 321 | } 322 | 323 | // When using OpenGL the `NSOpenGLView` needs to be resized separately? Why? Because 324 | // macOS. 325 | #[cfg(feature = "opengl")] 326 | if let Some(gl_context) = &self.inner.gl_context { 327 | gl_context.resize(size); 328 | } 329 | 330 | // If this is a standalone window then we'll also need to resize the window itself 331 | if let Some(ns_window) = self.inner.ns_window.get() { 332 | unsafe { NSWindow::setContentSize_(ns_window, size) }; 333 | } 334 | } 335 | } 336 | 337 | pub fn set_mouse_cursor(&mut self, _mouse_cursor: MouseCursor) { 338 | todo!() 339 | } 340 | 341 | #[cfg(feature = "opengl")] 342 | pub fn gl_context(&self) -> Option<&GlContext> { 343 | self.inner.gl_context.as_ref() 344 | } 345 | 346 | #[cfg(feature = "opengl")] 347 | fn create_gl_context(ns_window: Option, ns_view: id, config: GlConfig) -> GlContext { 348 | let mut handle = AppKitWindowHandle::empty(); 349 | handle.ns_window = ns_window.unwrap_or(ptr::null_mut()) as *mut c_void; 350 | handle.ns_view = ns_view as *mut c_void; 351 | let handle = RawWindowHandle::AppKit(handle); 352 | 353 | unsafe { GlContext::create(&handle, config).expect("Could not create OpenGL context") } 354 | } 355 | } 356 | 357 | pub(super) struct WindowState { 358 | pub(super) window_inner: WindowInner, 359 | window_handler: RefCell>, 360 | keyboard_state: KeyboardState, 361 | frame_timer: Cell>, 362 | /// The last known window info for this window. 363 | pub window_info: Cell, 364 | 365 | /// Events that will be triggered at the end of `window_handler`'s borrow. 366 | deferred_events: RefCell>, 367 | } 368 | 369 | impl WindowState { 370 | /// Gets the `WindowState` held by a given `NSView`. 371 | /// 372 | /// This method returns a cloned `Rc` rather than just a `&WindowState`, since the 373 | /// original `Rc` owned by the `NSView` can be dropped at any time 374 | /// (including during an event handler). 375 | pub(super) unsafe fn from_view(view: &Object) -> Rc { 376 | let state_ptr: *const c_void = *view.get_ivar(BASEVIEW_STATE_IVAR); 377 | 378 | let state_rc = Rc::from_raw(state_ptr as *const WindowState); 379 | let state = Rc::clone(&state_rc); 380 | let _ = Rc::into_raw(state_rc); 381 | 382 | state 383 | } 384 | 385 | /// Trigger the event immediately and return the event status. 386 | /// Will panic if `window_handler` is already borrowed (see `trigger_deferrable_event`). 387 | pub(super) fn trigger_event(&self, event: Event) -> EventStatus { 388 | let mut window = crate::Window::new(Window { inner: &self.window_inner }); 389 | let mut window_handler = self.window_handler.borrow_mut(); 390 | let status = window_handler.on_event(&mut window, event); 391 | self.send_deferred_events(window_handler.as_mut()); 392 | status 393 | } 394 | 395 | /// Trigger the event immediately if `window_handler` can be borrowed mutably, 396 | /// otherwise add the event to a queue that will be cleared once `window_handler`'s mutable borrow ends. 397 | /// As this method might result in the event triggering asynchronously, it can't reliably return the event status. 398 | pub(super) fn trigger_deferrable_event(&self, event: Event) { 399 | if let Ok(mut window_handler) = self.window_handler.try_borrow_mut() { 400 | let mut window = crate::Window::new(Window { inner: &self.window_inner }); 401 | window_handler.on_event(&mut window, event); 402 | self.send_deferred_events(window_handler.as_mut()); 403 | } else { 404 | self.deferred_events.borrow_mut().push_back(event); 405 | } 406 | } 407 | 408 | pub(super) fn trigger_frame(&self) { 409 | let mut window = crate::Window::new(Window { inner: &self.window_inner }); 410 | let mut window_handler = self.window_handler.borrow_mut(); 411 | window_handler.on_frame(&mut window); 412 | self.send_deferred_events(window_handler.as_mut()); 413 | } 414 | 415 | pub(super) fn keyboard_state(&self) -> &KeyboardState { 416 | &self.keyboard_state 417 | } 418 | 419 | pub(super) fn process_native_key_event(&self, event: *mut Object) -> Option { 420 | self.keyboard_state.process_native_event(event) 421 | } 422 | 423 | unsafe fn setup_timer(window_state_ptr: *const WindowState) { 424 | extern "C" fn timer_callback(_: *mut __CFRunLoopTimer, window_state_ptr: *mut c_void) { 425 | unsafe { 426 | let window_state = &*(window_state_ptr as *const WindowState); 427 | 428 | window_state.trigger_frame(); 429 | } 430 | } 431 | 432 | let mut timer_context = CFRunLoopTimerContext { 433 | version: 0, 434 | info: window_state_ptr as *mut c_void, 435 | retain: None, 436 | release: None, 437 | copyDescription: None, 438 | }; 439 | 440 | let timer = CFRunLoopTimer::new(0.0, 0.015, 0, 0, timer_callback, &mut timer_context); 441 | 442 | CFRunLoop::get_current().add_timer(&timer, kCFRunLoopDefaultMode); 443 | 444 | (*window_state_ptr).frame_timer.set(Some(timer)); 445 | } 446 | 447 | fn send_deferred_events(&self, window_handler: &mut dyn WindowHandler) { 448 | let mut window = crate::Window::new(Window { inner: &self.window_inner }); 449 | loop { 450 | let next_event = self.deferred_events.borrow_mut().pop_front(); 451 | if let Some(event) = next_event { 452 | window_handler.on_event(&mut window, event); 453 | } else { 454 | break; 455 | } 456 | } 457 | } 458 | } 459 | 460 | unsafe impl<'a> HasRawWindowHandle for Window<'a> { 461 | fn raw_window_handle(&self) -> RawWindowHandle { 462 | self.inner.raw_window_handle() 463 | } 464 | } 465 | 466 | unsafe impl<'a> HasRawDisplayHandle for Window<'a> { 467 | fn raw_display_handle(&self) -> RawDisplayHandle { 468 | RawDisplayHandle::AppKit(AppKitDisplayHandle::empty()) 469 | } 470 | } 471 | 472 | pub fn copy_to_clipboard(string: &str) { 473 | unsafe { 474 | let pb = NSPasteboard::generalPasteboard(nil); 475 | 476 | let ns_str = NSString::alloc(nil).init_str(string); 477 | 478 | pb.clearContents(); 479 | pb.setString_forType(ns_str, cocoa::appkit::NSPasteboardTypeString); 480 | } 481 | } 482 | -------------------------------------------------------------------------------- /src/mouse_cursor.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Eq, PartialEq, Clone, Copy, PartialOrd, Ord, Hash)] 2 | pub enum MouseCursor { 3 | Default, 4 | Hand, 5 | HandGrabbing, 6 | Help, 7 | 8 | Hidden, 9 | 10 | Text, 11 | VerticalText, 12 | 13 | Working, 14 | PtrWorking, 15 | 16 | NotAllowed, 17 | PtrNotAllowed, 18 | 19 | ZoomIn, 20 | ZoomOut, 21 | 22 | Alias, 23 | Copy, 24 | Move, 25 | AllScroll, 26 | Cell, 27 | Crosshair, 28 | 29 | EResize, 30 | NResize, 31 | NeResize, 32 | NwResize, 33 | SResize, 34 | SeResize, 35 | SwResize, 36 | WResize, 37 | EwResize, 38 | NsResize, 39 | NwseResize, 40 | NeswResize, 41 | ColResize, 42 | RowResize, 43 | } 44 | 45 | impl Default for MouseCursor { 46 | fn default() -> Self { 47 | Self::Default 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/win/cursor.rs: -------------------------------------------------------------------------------- 1 | use crate::MouseCursor; 2 | use winapi::{ 3 | shared::ntdef::LPCWSTR, 4 | um::winuser::{ 5 | IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP, IDC_IBEAM, IDC_NO, IDC_SIZEALL, 6 | IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE, IDC_WAIT, 7 | }, 8 | }; 9 | 10 | pub fn cursor_to_lpcwstr(cursor: MouseCursor) -> LPCWSTR { 11 | match cursor { 12 | MouseCursor::Default => IDC_ARROW, 13 | MouseCursor::Hand => IDC_HAND, 14 | MouseCursor::HandGrabbing => IDC_SIZEALL, 15 | MouseCursor::Help => IDC_HELP, 16 | // an empty LPCWSTR results in the cursor being hidden 17 | MouseCursor::Hidden => std::ptr::null(), 18 | 19 | MouseCursor::Text => IDC_IBEAM, 20 | MouseCursor::VerticalText => IDC_IBEAM, 21 | 22 | MouseCursor::Working => IDC_WAIT, 23 | MouseCursor::PtrWorking => IDC_APPSTARTING, 24 | 25 | MouseCursor::NotAllowed => IDC_NO, 26 | MouseCursor::PtrNotAllowed => IDC_NO, 27 | 28 | MouseCursor::ZoomIn => IDC_ARROW, 29 | MouseCursor::ZoomOut => IDC_ARROW, 30 | 31 | MouseCursor::Alias => IDC_ARROW, 32 | MouseCursor::Copy => IDC_ARROW, 33 | MouseCursor::Move => IDC_SIZEALL, 34 | MouseCursor::AllScroll => IDC_SIZEALL, 35 | MouseCursor::Cell => IDC_CROSS, 36 | MouseCursor::Crosshair => IDC_CROSS, 37 | 38 | MouseCursor::EResize => IDC_SIZEWE, 39 | MouseCursor::NResize => IDC_SIZENS, 40 | MouseCursor::NeResize => IDC_SIZENESW, 41 | MouseCursor::NwResize => IDC_SIZENWSE, 42 | MouseCursor::SResize => IDC_SIZENS, 43 | MouseCursor::SeResize => IDC_SIZENWSE, 44 | MouseCursor::SwResize => IDC_SIZENESW, 45 | MouseCursor::WResize => IDC_SIZEWE, 46 | MouseCursor::EwResize => IDC_SIZEWE, 47 | MouseCursor::NsResize => IDC_SIZENS, 48 | MouseCursor::NwseResize => IDC_SIZENWSE, 49 | MouseCursor::NeswResize => IDC_SIZENESW, 50 | 51 | MouseCursor::ColResize => IDC_SIZEWE, 52 | MouseCursor::RowResize => IDC_SIZENS, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/win/drop_target.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | use std::mem::transmute; 3 | use std::os::windows::prelude::OsStringExt; 4 | use std::ptr::null_mut; 5 | use std::rc::{Rc, Weak}; 6 | 7 | use winapi::shared::guiddef::{IsEqualIID, REFIID}; 8 | use winapi::shared::minwindef::{DWORD, WPARAM}; 9 | use winapi::shared::ntdef::{HRESULT, ULONG}; 10 | use winapi::shared::windef::{POINT, POINTL}; 11 | use winapi::shared::winerror::{E_NOINTERFACE, E_UNEXPECTED, S_OK}; 12 | use winapi::shared::wtypes::DVASPECT_CONTENT; 13 | use winapi::um::objidl::{IDataObject, FORMATETC, STGMEDIUM, TYMED_HGLOBAL}; 14 | use winapi::um::oleidl::{ 15 | IDropTarget, IDropTargetVtbl, DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, 16 | DROPEFFECT_NONE, DROPEFFECT_SCROLL, 17 | }; 18 | use winapi::um::shellapi::{DragQueryFileW, HDROP}; 19 | use winapi::um::unknwnbase::{IUnknown, IUnknownVtbl}; 20 | use winapi::um::winuser::{ScreenToClient, CF_HDROP}; 21 | use winapi::Interface; 22 | 23 | use crate::{DropData, DropEffect, Event, EventStatus, MouseEvent, PhyPoint, Point}; 24 | 25 | use super::WindowState; 26 | 27 | // These function pointers have to be stored in a (const) variable before they can be transmuted 28 | // Transmuting is needed because winapi has a bug where the pt parameter has an incorrect 29 | // type `*const POINTL` 30 | const DRAG_ENTER_PTR: unsafe extern "system" fn( 31 | this: *mut IDropTarget, 32 | pDataObj: *const IDataObject, 33 | grfKeyState: DWORD, 34 | pt: POINTL, 35 | pdwEffect: *mut DWORD, 36 | ) -> HRESULT = DropTarget::drag_enter; 37 | const DRAG_OVER_PTR: unsafe extern "system" fn( 38 | this: *mut IDropTarget, 39 | grfKeyState: DWORD, 40 | pt: POINTL, 41 | pdwEffect: *mut DWORD, 42 | ) -> HRESULT = DropTarget::drag_over; 43 | const DROP_PTR: unsafe extern "system" fn( 44 | this: *mut IDropTarget, 45 | pDataObj: *const IDataObject, 46 | grfKeyState: DWORD, 47 | pt: POINTL, 48 | pdwEffect: *mut DWORD, 49 | ) -> HRESULT = DropTarget::drop; 50 | 51 | #[allow(clippy::missing_transmute_annotations)] 52 | const DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl { 53 | parent: IUnknownVtbl { 54 | QueryInterface: DropTarget::query_interface, 55 | AddRef: DropTarget::add_ref, 56 | Release: DropTarget::release, 57 | }, 58 | DragEnter: unsafe { transmute(DRAG_ENTER_PTR) }, 59 | DragOver: unsafe { transmute(DRAG_OVER_PTR) }, 60 | DragLeave: DropTarget::drag_leave, 61 | Drop: unsafe { transmute(DROP_PTR) }, 62 | }; 63 | 64 | #[repr(C)] 65 | pub(super) struct DropTarget { 66 | base: IDropTarget, 67 | 68 | window_state: Weak, 69 | 70 | // These are cached since DragOver and DragLeave callbacks don't provide them, 71 | // and handling drag move events gets awkward on the client end otherwise 72 | drag_position: Point, 73 | drop_data: DropData, 74 | } 75 | 76 | impl DropTarget { 77 | pub(super) fn new(window_state: Weak) -> Self { 78 | Self { 79 | base: IDropTarget { lpVtbl: &DROP_TARGET_VTBL }, 80 | 81 | window_state, 82 | 83 | drag_position: Point::new(0.0, 0.0), 84 | drop_data: DropData::None, 85 | } 86 | } 87 | 88 | #[allow(non_snake_case)] 89 | fn on_event(&self, pdwEffect: Option<*mut DWORD>, event: MouseEvent) { 90 | let Some(window_state) = self.window_state.upgrade() else { 91 | return; 92 | }; 93 | 94 | unsafe { 95 | let mut window = crate::Window::new(window_state.create_window()); 96 | 97 | let event = Event::Mouse(event); 98 | let event_status = 99 | window_state.handler_mut().as_mut().unwrap().on_event(&mut window, event); 100 | 101 | if let Some(pdwEffect) = pdwEffect { 102 | match event_status { 103 | EventStatus::AcceptDrop(DropEffect::Copy) => *pdwEffect = DROPEFFECT_COPY, 104 | EventStatus::AcceptDrop(DropEffect::Move) => *pdwEffect = DROPEFFECT_MOVE, 105 | EventStatus::AcceptDrop(DropEffect::Link) => *pdwEffect = DROPEFFECT_LINK, 106 | EventStatus::AcceptDrop(DropEffect::Scroll) => *pdwEffect = DROPEFFECT_SCROLL, 107 | _ => *pdwEffect = DROPEFFECT_NONE, 108 | } 109 | } 110 | } 111 | } 112 | 113 | fn parse_coordinates(&mut self, pt: POINTL) { 114 | let Some(window_state) = self.window_state.upgrade() else { 115 | return; 116 | }; 117 | let mut pt = POINT { x: pt.x, y: pt.y }; 118 | unsafe { ScreenToClient(window_state.hwnd, &mut pt as *mut POINT) }; 119 | let phy_point = PhyPoint::new(pt.x, pt.y); 120 | self.drag_position = phy_point.to_logical(&window_state.window_info()); 121 | } 122 | 123 | fn parse_drop_data(&mut self, data_object: &IDataObject) { 124 | let format = FORMATETC { 125 | cfFormat: CF_HDROP as u16, 126 | ptd: null_mut(), 127 | dwAspect: DVASPECT_CONTENT, 128 | lindex: -1, 129 | tymed: TYMED_HGLOBAL, 130 | }; 131 | 132 | let mut medium = STGMEDIUM { tymed: 0, u: null_mut(), pUnkForRelease: null_mut() }; 133 | 134 | unsafe { 135 | let hresult = data_object.GetData(&format, &mut medium); 136 | if hresult != S_OK { 137 | self.drop_data = DropData::None; 138 | return; 139 | } 140 | 141 | let hdrop = *(*medium.u).hGlobal() as HDROP; 142 | 143 | let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, null_mut(), 0); 144 | if item_count == 0 { 145 | self.drop_data = DropData::None; 146 | return; 147 | } 148 | 149 | let mut paths = Vec::with_capacity(item_count as usize); 150 | 151 | for i in 0..item_count { 152 | let characters = DragQueryFileW(hdrop, i, null_mut(), 0); 153 | let buffer_size = characters as usize + 1; 154 | let mut buffer = vec![0u16; buffer_size]; 155 | 156 | DragQueryFileW(hdrop, i, buffer.as_mut_ptr().cast(), buffer_size as u32); 157 | 158 | paths.push(OsString::from_wide(&buffer[..characters as usize]).into()) 159 | } 160 | 161 | self.drop_data = DropData::Files(paths); 162 | } 163 | } 164 | 165 | #[allow(non_snake_case)] 166 | unsafe extern "system" fn query_interface( 167 | this: *mut IUnknown, riid: REFIID, ppvObject: *mut *mut winapi::ctypes::c_void, 168 | ) -> HRESULT { 169 | if IsEqualIID(&*riid, &IUnknown::uuidof()) || IsEqualIID(&*riid, &IDropTarget::uuidof()) { 170 | Self::add_ref(this); 171 | *ppvObject = this as *mut winapi::ctypes::c_void; 172 | return S_OK; 173 | } 174 | 175 | E_NOINTERFACE 176 | } 177 | 178 | unsafe extern "system" fn add_ref(this: *mut IUnknown) -> ULONG { 179 | let arc = Rc::from_raw(this); 180 | let result = Rc::strong_count(&arc) + 1; 181 | let _ = Rc::into_raw(arc); 182 | 183 | Rc::increment_strong_count(this); 184 | 185 | result as ULONG 186 | } 187 | 188 | unsafe extern "system" fn release(this: *mut IUnknown) -> ULONG { 189 | let arc = Rc::from_raw(this); 190 | let result = Rc::strong_count(&arc) - 1; 191 | let _ = Rc::into_raw(arc); 192 | 193 | Rc::decrement_strong_count(this); 194 | 195 | result as ULONG 196 | } 197 | 198 | #[allow(non_snake_case)] 199 | unsafe extern "system" fn drag_enter( 200 | this: *mut IDropTarget, pDataObj: *const IDataObject, grfKeyState: DWORD, pt: POINTL, 201 | pdwEffect: *mut DWORD, 202 | ) -> HRESULT { 203 | let drop_target = &mut *(this as *mut DropTarget); 204 | let Some(window_state) = drop_target.window_state.upgrade() else { 205 | return E_UNEXPECTED; 206 | }; 207 | 208 | let modifiers = 209 | window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM); 210 | 211 | drop_target.parse_coordinates(pt); 212 | drop_target.parse_drop_data(&*pDataObj); 213 | 214 | let event = MouseEvent::DragEntered { 215 | position: drop_target.drag_position, 216 | modifiers, 217 | data: drop_target.drop_data.clone(), 218 | }; 219 | 220 | drop_target.on_event(Some(pdwEffect), event); 221 | S_OK 222 | } 223 | 224 | #[allow(non_snake_case)] 225 | unsafe extern "system" fn drag_over( 226 | this: *mut IDropTarget, grfKeyState: DWORD, pt: POINTL, pdwEffect: *mut DWORD, 227 | ) -> HRESULT { 228 | let drop_target = &mut *(this as *mut DropTarget); 229 | let Some(window_state) = drop_target.window_state.upgrade() else { 230 | return E_UNEXPECTED; 231 | }; 232 | 233 | let modifiers = 234 | window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM); 235 | 236 | drop_target.parse_coordinates(pt); 237 | 238 | let event = MouseEvent::DragMoved { 239 | position: drop_target.drag_position, 240 | modifiers, 241 | data: drop_target.drop_data.clone(), 242 | }; 243 | 244 | drop_target.on_event(Some(pdwEffect), event); 245 | S_OK 246 | } 247 | 248 | unsafe extern "system" fn drag_leave(this: *mut IDropTarget) -> HRESULT { 249 | let drop_target = &mut *(this as *mut DropTarget); 250 | drop_target.on_event(None, MouseEvent::DragLeft); 251 | S_OK 252 | } 253 | 254 | #[allow(non_snake_case)] 255 | unsafe extern "system" fn drop( 256 | this: *mut IDropTarget, pDataObj: *const IDataObject, grfKeyState: DWORD, pt: POINTL, 257 | pdwEffect: *mut DWORD, 258 | ) -> HRESULT { 259 | let drop_target = &mut *(this as *mut DropTarget); 260 | let Some(window_state) = drop_target.window_state.upgrade() else { 261 | return E_UNEXPECTED; 262 | }; 263 | 264 | let modifiers = 265 | window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM); 266 | 267 | drop_target.parse_coordinates(pt); 268 | drop_target.parse_drop_data(&*pDataObj); 269 | 270 | let event = MouseEvent::DragDropped { 271 | position: drop_target.drag_position, 272 | modifiers, 273 | data: drop_target.drop_data.clone(), 274 | }; 275 | 276 | drop_target.on_event(Some(pdwEffect), event); 277 | S_OK 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/win/mod.rs: -------------------------------------------------------------------------------- 1 | mod cursor; 2 | mod drop_target; 3 | mod keyboard; 4 | mod window; 5 | 6 | pub use window::*; 7 | -------------------------------------------------------------------------------- /src/window.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use raw_window_handle::{ 4 | HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, 5 | }; 6 | 7 | use crate::event::{Event, EventStatus}; 8 | use crate::window_open_options::WindowOpenOptions; 9 | use crate::{MouseCursor, Size}; 10 | 11 | #[cfg(target_os = "macos")] 12 | use crate::macos as platform; 13 | #[cfg(target_os = "windows")] 14 | use crate::win as platform; 15 | #[cfg(target_os = "linux")] 16 | use crate::x11 as platform; 17 | 18 | pub struct WindowHandle { 19 | window_handle: platform::WindowHandle, 20 | // so that WindowHandle is !Send on all platforms 21 | phantom: PhantomData<*mut ()>, 22 | } 23 | 24 | impl WindowHandle { 25 | fn new(window_handle: platform::WindowHandle) -> Self { 26 | Self { window_handle, phantom: PhantomData } 27 | } 28 | 29 | /// Close the window 30 | pub fn close(&mut self) { 31 | self.window_handle.close(); 32 | } 33 | 34 | /// Returns `true` if the window is still open, and returns `false` 35 | /// if the window was closed/dropped. 36 | pub fn is_open(&self) -> bool { 37 | self.window_handle.is_open() 38 | } 39 | } 40 | 41 | unsafe impl HasRawWindowHandle for WindowHandle { 42 | fn raw_window_handle(&self) -> RawWindowHandle { 43 | self.window_handle.raw_window_handle() 44 | } 45 | } 46 | 47 | pub trait WindowHandler { 48 | fn on_frame(&mut self, window: &mut Window); 49 | fn on_event(&mut self, window: &mut Window, event: Event) -> EventStatus; 50 | } 51 | 52 | pub struct Window<'a> { 53 | window: platform::Window<'a>, 54 | 55 | // so that Window is !Send on all platforms 56 | phantom: PhantomData<*mut ()>, 57 | } 58 | 59 | impl<'a> Window<'a> { 60 | #[cfg(target_os = "windows")] 61 | pub(crate) fn new(window: platform::Window<'a>) -> Window<'a> { 62 | Window { window, phantom: PhantomData } 63 | } 64 | 65 | #[cfg(not(target_os = "windows"))] 66 | pub(crate) fn new(window: platform::Window) -> Window { 67 | Window { window, phantom: PhantomData } 68 | } 69 | 70 | pub fn open_parented(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle 71 | where 72 | P: HasRawWindowHandle, 73 | H: WindowHandler + 'static, 74 | B: FnOnce(&mut Window) -> H, 75 | B: Send + 'static, 76 | { 77 | let window_handle = platform::Window::open_parented::(parent, options, build); 78 | WindowHandle::new(window_handle) 79 | } 80 | 81 | pub fn open_blocking(options: WindowOpenOptions, build: B) 82 | where 83 | H: WindowHandler + 'static, 84 | B: FnOnce(&mut Window) -> H, 85 | B: Send + 'static, 86 | { 87 | platform::Window::open_blocking::(options, build) 88 | } 89 | 90 | /// Close the window 91 | pub fn close(&mut self) { 92 | self.window.close(); 93 | } 94 | 95 | /// Resize the window to the given size. The size is always in logical pixels. DPI scaling will 96 | /// automatically be accounted for. 97 | pub fn resize(&mut self, size: Size) { 98 | self.window.resize(size); 99 | } 100 | 101 | pub fn set_mouse_cursor(&mut self, cursor: MouseCursor) { 102 | self.window.set_mouse_cursor(cursor); 103 | } 104 | 105 | pub fn has_focus(&mut self) -> bool { 106 | self.window.has_focus() 107 | } 108 | 109 | pub fn focus(&mut self) { 110 | self.window.focus() 111 | } 112 | 113 | /// If provided, then an OpenGL context will be created for this window. You'll be able to 114 | /// access this context through [crate::Window::gl_context]. 115 | #[cfg(feature = "opengl")] 116 | pub fn gl_context(&self) -> Option<&crate::gl::GlContext> { 117 | self.window.gl_context() 118 | } 119 | } 120 | 121 | unsafe impl<'a> HasRawWindowHandle for Window<'a> { 122 | fn raw_window_handle(&self) -> RawWindowHandle { 123 | self.window.raw_window_handle() 124 | } 125 | } 126 | 127 | unsafe impl<'a> HasRawDisplayHandle for Window<'a> { 128 | fn raw_display_handle(&self) -> RawDisplayHandle { 129 | self.window.raw_display_handle() 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/window_info.rs: -------------------------------------------------------------------------------- 1 | /// The info about the window 2 | #[derive(Debug, Copy, Clone)] 3 | pub struct WindowInfo { 4 | logical_size: Size, 5 | physical_size: PhySize, 6 | scale: f64, 7 | scale_recip: f64, 8 | } 9 | 10 | impl WindowInfo { 11 | pub fn from_logical_size(logical_size: Size, scale: f64) -> Self { 12 | let scale_recip = if scale == 1.0 { 1.0 } else { 1.0 / scale }; 13 | 14 | let physical_size = PhySize { 15 | width: (logical_size.width * scale).round() as u32, 16 | height: (logical_size.height * scale).round() as u32, 17 | }; 18 | 19 | Self { logical_size, physical_size, scale, scale_recip } 20 | } 21 | 22 | pub fn from_physical_size(physical_size: PhySize, scale: f64) -> Self { 23 | let scale_recip = if scale == 1.0 { 1.0 } else { 1.0 / scale }; 24 | 25 | let logical_size = Size { 26 | width: f64::from(physical_size.width) * scale_recip, 27 | height: f64::from(physical_size.height) * scale_recip, 28 | }; 29 | 30 | Self { logical_size, physical_size, scale, scale_recip } 31 | } 32 | 33 | /// The logical size of the window 34 | pub fn logical_size(&self) -> Size { 35 | self.logical_size 36 | } 37 | 38 | /// The physical size of the window 39 | pub fn physical_size(&self) -> PhySize { 40 | self.physical_size 41 | } 42 | 43 | /// The scale factor of the window 44 | pub fn scale(&self) -> f64 { 45 | self.scale 46 | } 47 | 48 | /// The reciprocal of the scale factor of the window 49 | pub fn scale_recip(&self) -> f64 { 50 | self.scale_recip 51 | } 52 | } 53 | 54 | /// A point in logical coordinates 55 | #[derive(Debug, Copy, Clone, PartialEq)] 56 | pub struct Point { 57 | pub x: f64, 58 | pub y: f64, 59 | } 60 | 61 | impl Point { 62 | /// Create a new point in logical coordinates 63 | pub fn new(x: f64, y: f64) -> Self { 64 | Self { x, y } 65 | } 66 | 67 | /// Convert to actual physical coordinates 68 | #[inline] 69 | pub fn to_physical(&self, window_info: &WindowInfo) -> PhyPoint { 70 | PhyPoint { 71 | x: (self.x * window_info.scale()).round() as i32, 72 | y: (self.y * window_info.scale()).round() as i32, 73 | } 74 | } 75 | } 76 | 77 | /// A point in actual physical coordinates 78 | #[derive(Debug, Copy, Clone, PartialEq)] 79 | pub struct PhyPoint { 80 | pub x: i32, 81 | pub y: i32, 82 | } 83 | 84 | impl PhyPoint { 85 | /// Create a new point in actual physical coordinates 86 | pub fn new(x: i32, y: i32) -> Self { 87 | Self { x, y } 88 | } 89 | 90 | /// Convert to logical coordinates 91 | #[inline] 92 | pub fn to_logical(&self, window_info: &WindowInfo) -> Point { 93 | Point { 94 | x: f64::from(self.x) * window_info.scale_recip(), 95 | y: f64::from(self.y) * window_info.scale_recip(), 96 | } 97 | } 98 | } 99 | 100 | /// A size in logical coordinates 101 | #[derive(Debug, Copy, Clone, PartialEq)] 102 | pub struct Size { 103 | pub width: f64, 104 | pub height: f64, 105 | } 106 | 107 | impl Size { 108 | /// Create a new size in logical coordinates 109 | pub fn new(width: f64, height: f64) -> Self { 110 | Self { width, height } 111 | } 112 | 113 | /// Convert to actual physical size 114 | #[inline] 115 | pub fn to_physical(&self, window_info: &WindowInfo) -> PhySize { 116 | PhySize { 117 | width: (self.width * window_info.scale()).round() as u32, 118 | height: (self.height * window_info.scale()).round() as u32, 119 | } 120 | } 121 | } 122 | 123 | /// An actual size in physical coordinates 124 | #[derive(Debug, Copy, Clone, PartialEq)] 125 | pub struct PhySize { 126 | pub width: u32, 127 | pub height: u32, 128 | } 129 | 130 | impl PhySize { 131 | /// Create a new size in actual physical coordinates 132 | pub fn new(width: u32, height: u32) -> Self { 133 | Self { width, height } 134 | } 135 | 136 | /// Convert to logical size 137 | #[inline] 138 | pub fn to_logical(&self, window_info: &WindowInfo) -> Size { 139 | Size { 140 | width: f64::from(self.width) * window_info.scale_recip(), 141 | height: f64::from(self.height) * window_info.scale_recip(), 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/window_open_options.rs: -------------------------------------------------------------------------------- 1 | use crate::Size; 2 | 3 | /// The dpi scaling policy of the window 4 | #[derive(Debug, Clone, Copy, PartialEq)] 5 | pub enum WindowScalePolicy { 6 | /// Use the system's dpi scale factor 7 | SystemScaleFactor, 8 | /// Use the given dpi scale factor (e.g. `1.0` = 96 dpi) 9 | ScaleFactor(f64), 10 | } 11 | 12 | /// The options for opening a new window 13 | pub struct WindowOpenOptions { 14 | pub title: String, 15 | 16 | /// The logical size of the window. 17 | /// 18 | /// These dimensions will be scaled by the scaling policy specified in `scale`. Mouse 19 | /// position will be passed back as logical coordinates. 20 | pub size: Size, 21 | 22 | /// The dpi scaling policy 23 | pub scale: WindowScalePolicy, 24 | 25 | /// If provided, then an OpenGL context will be created for this window. You'll be able to 26 | /// access this context through [crate::Window::gl_context]. 27 | #[cfg(feature = "opengl")] 28 | pub gl_config: Option, 29 | } 30 | -------------------------------------------------------------------------------- /src/x11/cursor.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use x11rb::connection::Connection; 4 | use x11rb::cursor::Handle as CursorHandle; 5 | use x11rb::protocol::xproto::{ConnectionExt as _, Cursor}; 6 | use x11rb::xcb_ffi::XCBConnection; 7 | 8 | use crate::MouseCursor; 9 | 10 | fn create_empty_cursor(conn: &XCBConnection, screen: usize) -> Result> { 11 | let cursor_id = conn.generate_id()?; 12 | let pixmap_id = conn.generate_id()?; 13 | let root_window = conn.setup().roots[screen].root; 14 | conn.create_pixmap(1, pixmap_id, root_window, 1, 1)?; 15 | conn.create_cursor(cursor_id, pixmap_id, pixmap_id, 0, 0, 0, 0, 0, 0, 0, 0)?; 16 | conn.free_pixmap(pixmap_id)?; 17 | 18 | Ok(cursor_id) 19 | } 20 | 21 | fn load_cursor( 22 | conn: &XCBConnection, cursor_handle: &CursorHandle, name: &str, 23 | ) -> Result, Box> { 24 | let cursor = cursor_handle.load_cursor(conn, name)?; 25 | if cursor != x11rb::NONE { 26 | Ok(Some(cursor)) 27 | } else { 28 | Ok(None) 29 | } 30 | } 31 | 32 | fn load_first_existing_cursor( 33 | conn: &XCBConnection, cursor_handle: &CursorHandle, names: &[&str], 34 | ) -> Result, Box> { 35 | for name in names { 36 | let cursor = load_cursor(conn, cursor_handle, name)?; 37 | if cursor.is_some() { 38 | return Ok(cursor); 39 | } 40 | } 41 | 42 | Ok(None) 43 | } 44 | 45 | pub(super) fn get_xcursor( 46 | conn: &XCBConnection, screen: usize, cursor_handle: &CursorHandle, cursor: MouseCursor, 47 | ) -> Result> { 48 | let load = |name: &str| load_cursor(conn, cursor_handle, name); 49 | let loadn = |names: &[&str]| load_first_existing_cursor(conn, cursor_handle, names); 50 | 51 | let cursor = match cursor { 52 | MouseCursor::Default => None, // catch this in the fallback case below 53 | 54 | MouseCursor::Hand => loadn(&["hand2", "hand1"])?, 55 | MouseCursor::HandGrabbing => loadn(&["closedhand", "grabbing"])?, 56 | MouseCursor::Help => load("question_arrow")?, 57 | 58 | MouseCursor::Hidden => Some(create_empty_cursor(conn, screen)?), 59 | 60 | MouseCursor::Text => loadn(&["text", "xterm"])?, 61 | MouseCursor::VerticalText => load("vertical-text")?, 62 | 63 | MouseCursor::Working => load("watch")?, 64 | MouseCursor::PtrWorking => load("left_ptr_watch")?, 65 | 66 | MouseCursor::NotAllowed => load("crossed_circle")?, 67 | MouseCursor::PtrNotAllowed => loadn(&["no-drop", "crossed_circle"])?, 68 | 69 | MouseCursor::ZoomIn => load("zoom-in")?, 70 | MouseCursor::ZoomOut => load("zoom-out")?, 71 | 72 | MouseCursor::Alias => load("link")?, 73 | MouseCursor::Copy => load("copy")?, 74 | MouseCursor::Move => load("move")?, 75 | MouseCursor::AllScroll => load("all-scroll")?, 76 | MouseCursor::Cell => load("plus")?, 77 | MouseCursor::Crosshair => load("crosshair")?, 78 | 79 | MouseCursor::EResize => load("right_side")?, 80 | MouseCursor::NResize => load("top_side")?, 81 | MouseCursor::NeResize => load("top_right_corner")?, 82 | MouseCursor::NwResize => load("top_left_corner")?, 83 | MouseCursor::SResize => load("bottom_side")?, 84 | MouseCursor::SeResize => load("bottom_right_corner")?, 85 | MouseCursor::SwResize => load("bottom_left_corner")?, 86 | MouseCursor::WResize => load("left_side")?, 87 | MouseCursor::EwResize => load("h_double_arrow")?, 88 | MouseCursor::NsResize => load("v_double_arrow")?, 89 | MouseCursor::NwseResize => loadn(&["bd_double_arrow", "size_bdiag"])?, 90 | MouseCursor::NeswResize => loadn(&["fd_double_arrow", "size_fdiag"])?, 91 | MouseCursor::ColResize => loadn(&["split_h", "h_double_arrow"])?, 92 | MouseCursor::RowResize => loadn(&["split_v", "v_double_arrow"])?, 93 | }; 94 | 95 | if let Some(cursor) = cursor { 96 | Ok(cursor) 97 | } else { 98 | Ok(load("left_ptr")?.unwrap_or(x11rb::NONE)) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/x11/event_loop.rs: -------------------------------------------------------------------------------- 1 | use crate::x11::keyboard::{convert_key_press_event, convert_key_release_event, key_mods}; 2 | use crate::x11::{ParentHandle, Window, WindowInner}; 3 | use crate::{ 4 | Event, MouseButton, MouseEvent, PhyPoint, PhySize, ScrollDelta, WindowEvent, WindowHandler, 5 | WindowInfo, 6 | }; 7 | use std::error::Error; 8 | use std::os::fd::AsRawFd; 9 | use std::time::{Duration, Instant}; 10 | use x11rb::connection::Connection; 11 | use x11rb::protocol::Event as XEvent; 12 | 13 | pub(super) struct EventLoop { 14 | handler: Box, 15 | window: WindowInner, 16 | parent_handle: Option, 17 | 18 | new_physical_size: Option, 19 | frame_interval: Duration, 20 | event_loop_running: bool, 21 | } 22 | 23 | impl EventLoop { 24 | pub fn new( 25 | window: WindowInner, handler: impl WindowHandler + 'static, 26 | parent_handle: Option, 27 | ) -> Self { 28 | Self { 29 | window, 30 | handler: Box::new(handler), 31 | parent_handle, 32 | frame_interval: Duration::from_millis(15), 33 | event_loop_running: false, 34 | new_physical_size: None, 35 | } 36 | } 37 | 38 | #[inline] 39 | fn drain_xcb_events(&mut self) -> Result<(), Box> { 40 | // the X server has a tendency to send spurious/extraneous configure notify events when a 41 | // window is resized, and we need to batch those together and just send one resize event 42 | // when they've all been coalesced. 43 | self.new_physical_size = None; 44 | 45 | while let Some(event) = self.window.xcb_connection.conn.poll_for_event()? { 46 | self.handle_xcb_event(event); 47 | } 48 | 49 | if let Some(size) = self.new_physical_size.take() { 50 | self.window.window_info = 51 | WindowInfo::from_physical_size(size, self.window.window_info.scale()); 52 | 53 | let window_info = self.window.window_info; 54 | 55 | self.handler.on_event( 56 | &mut crate::Window::new(Window { inner: &self.window }), 57 | Event::Window(WindowEvent::Resized(window_info)), 58 | ); 59 | } 60 | 61 | Ok(()) 62 | } 63 | 64 | // Event loop 65 | // FIXME: poll() acts fine on linux, sometimes funky on *BSD. XCB upstream uses a define to 66 | // switch between poll() and select() (the latter of which is fine on *BSD), and we should do 67 | // the same. 68 | pub fn run(&mut self) -> Result<(), Box> { 69 | use nix::poll::*; 70 | 71 | let xcb_fd = self.window.xcb_connection.conn.as_raw_fd(); 72 | 73 | let mut last_frame = Instant::now(); 74 | self.event_loop_running = true; 75 | 76 | while self.event_loop_running { 77 | // We'll try to keep a consistent frame pace. If the last frame couldn't be processed in 78 | // the expected frame time, this will throttle down to prevent multiple frames from 79 | // being queued up. The conditional here is needed because event handling and frame 80 | // drawing is interleaved. The `poll()` function below will wait until the next frame 81 | // can be drawn, or until the window receives an event. We thus need to manually check 82 | // if it's already time to draw a new frame. 83 | let next_frame = last_frame + self.frame_interval; 84 | if Instant::now() >= next_frame { 85 | self.handler.on_frame(&mut crate::Window::new(Window { inner: &self.window })); 86 | last_frame = Instant::max(next_frame, Instant::now() - self.frame_interval); 87 | } 88 | 89 | let mut fds = [PollFd::new(xcb_fd, PollFlags::POLLIN)]; 90 | 91 | // Check for any events in the internal buffers 92 | // before going to sleep: 93 | self.drain_xcb_events()?; 94 | 95 | // FIXME: handle errors 96 | poll(&mut fds, next_frame.duration_since(Instant::now()).subsec_millis() as i32) 97 | .unwrap(); 98 | 99 | if let Some(revents) = fds[0].revents() { 100 | if revents.contains(PollFlags::POLLERR) { 101 | panic!("xcb connection poll error"); 102 | } 103 | 104 | if revents.contains(PollFlags::POLLIN) { 105 | self.drain_xcb_events()?; 106 | } 107 | } 108 | 109 | // Check if the parents's handle was dropped (such as when the host 110 | // requested the window to close) 111 | // 112 | // FIXME: This will need to be changed from just setting an atomic to somehow 113 | // synchronizing with the window being closed (using a synchronous channel, or 114 | // by joining on the event loop thread). 115 | if let Some(parent_handle) = &self.parent_handle { 116 | if parent_handle.parent_did_drop() { 117 | self.handle_must_close(); 118 | self.window.close_requested.set(false); 119 | } 120 | } 121 | 122 | // Check if the user has requested the window to close 123 | if self.window.close_requested.get() { 124 | self.handle_must_close(); 125 | self.window.close_requested.set(false); 126 | } 127 | } 128 | 129 | Ok(()) 130 | } 131 | 132 | fn handle_xcb_event(&mut self, event: XEvent) { 133 | // For all the keyboard and mouse events, you can fetch 134 | // `x`, `y`, `detail`, and `state`. 135 | // - `x` and `y` are the position inside the window where the cursor currently is 136 | // when the event happened. 137 | // - `detail` will tell you which keycode was pressed/released (for keyboard events) 138 | // or which mouse button was pressed/released (for mouse events). 139 | // For mouse events, here's what the value means (at least on my current mouse): 140 | // 1 = left mouse button 141 | // 2 = middle mouse button (scroll wheel) 142 | // 3 = right mouse button 143 | // 4 = scroll wheel up 144 | // 5 = scroll wheel down 145 | // 8 = lower side button ("back" button) 146 | // 9 = upper side button ("forward" button) 147 | // Note that you *will* get a "button released" event for even the scroll wheel 148 | // events, which you can probably ignore. 149 | // - `state` will tell you the state of the main three mouse buttons and some of 150 | // the keyboard modifier keys at the time of the event. 151 | // http://rtbo.github.io/rust-xcb/src/xcb/ffi/xproto.rs.html#445 152 | 153 | match event { 154 | //// 155 | // window 156 | //// 157 | XEvent::ClientMessage(event) => { 158 | if event.format == 32 159 | && event.data.as_data32()[0] 160 | == self.window.xcb_connection.atoms.WM_DELETE_WINDOW 161 | { 162 | self.handle_close_requested(); 163 | } 164 | } 165 | 166 | XEvent::ConfigureNotify(event) => { 167 | let new_physical_size = PhySize::new(event.width as u32, event.height as u32); 168 | 169 | if self.new_physical_size.is_some() 170 | || new_physical_size != self.window.window_info.physical_size() 171 | { 172 | self.new_physical_size = Some(new_physical_size); 173 | } 174 | } 175 | 176 | //// 177 | // mouse 178 | //// 179 | XEvent::MotionNotify(event) => { 180 | let physical_pos = PhyPoint::new(event.event_x as i32, event.event_y as i32); 181 | let logical_pos = physical_pos.to_logical(&self.window.window_info); 182 | 183 | self.handler.on_event( 184 | &mut crate::Window::new(Window { inner: &self.window }), 185 | Event::Mouse(MouseEvent::CursorMoved { 186 | position: logical_pos, 187 | modifiers: key_mods(event.state), 188 | }), 189 | ); 190 | } 191 | 192 | XEvent::EnterNotify(event) => { 193 | self.handler.on_event( 194 | &mut crate::Window::new(Window { inner: &self.window }), 195 | Event::Mouse(MouseEvent::CursorEntered), 196 | ); 197 | // since no `MOTION_NOTIFY` event is generated when `ENTER_NOTIFY` is generated, 198 | // we generate a CursorMoved as well, so the mouse position from here isn't lost 199 | let physical_pos = PhyPoint::new(event.event_x as i32, event.event_y as i32); 200 | let logical_pos = physical_pos.to_logical(&self.window.window_info); 201 | self.handler.on_event( 202 | &mut crate::Window::new(Window { inner: &self.window }), 203 | Event::Mouse(MouseEvent::CursorMoved { 204 | position: logical_pos, 205 | modifiers: key_mods(event.state), 206 | }), 207 | ); 208 | } 209 | 210 | XEvent::LeaveNotify(_) => { 211 | self.handler.on_event( 212 | &mut crate::Window::new(Window { inner: &self.window }), 213 | Event::Mouse(MouseEvent::CursorLeft), 214 | ); 215 | } 216 | 217 | XEvent::ButtonPress(event) => match event.detail { 218 | 4..=7 => { 219 | self.handler.on_event( 220 | &mut crate::Window::new(Window { inner: &self.window }), 221 | Event::Mouse(MouseEvent::WheelScrolled { 222 | delta: match event.detail { 223 | 4 => ScrollDelta::Lines { x: 0.0, y: 1.0 }, 224 | 5 => ScrollDelta::Lines { x: 0.0, y: -1.0 }, 225 | 6 => ScrollDelta::Lines { x: -1.0, y: 0.0 }, 226 | 7 => ScrollDelta::Lines { x: 1.0, y: 0.0 }, 227 | _ => unreachable!(), 228 | }, 229 | modifiers: key_mods(event.state), 230 | }), 231 | ); 232 | } 233 | detail => { 234 | let button_id = mouse_id(detail); 235 | self.handler.on_event( 236 | &mut crate::Window::new(Window { inner: &self.window }), 237 | Event::Mouse(MouseEvent::ButtonPressed { 238 | button: button_id, 239 | modifiers: key_mods(event.state), 240 | }), 241 | ); 242 | } 243 | }, 244 | 245 | XEvent::ButtonRelease(event) => { 246 | if !(4..=7).contains(&event.detail) { 247 | let button_id = mouse_id(event.detail); 248 | self.handler.on_event( 249 | &mut crate::Window::new(Window { inner: &self.window }), 250 | Event::Mouse(MouseEvent::ButtonReleased { 251 | button: button_id, 252 | modifiers: key_mods(event.state), 253 | }), 254 | ); 255 | } 256 | } 257 | 258 | //// 259 | // keys 260 | //// 261 | XEvent::KeyPress(event) => { 262 | self.handler.on_event( 263 | &mut crate::Window::new(Window { inner: &self.window }), 264 | Event::Keyboard(convert_key_press_event(&event)), 265 | ); 266 | } 267 | 268 | XEvent::KeyRelease(event) => { 269 | self.handler.on_event( 270 | &mut crate::Window::new(Window { inner: &self.window }), 271 | Event::Keyboard(convert_key_release_event(&event)), 272 | ); 273 | } 274 | 275 | _ => {} 276 | } 277 | } 278 | 279 | fn handle_close_requested(&mut self) { 280 | // FIXME: handler should decide whether window stays open or not 281 | self.handle_must_close(); 282 | } 283 | 284 | fn handle_must_close(&mut self) { 285 | self.handler.on_event( 286 | &mut crate::Window::new(Window { inner: &self.window }), 287 | Event::Window(WindowEvent::WillClose), 288 | ); 289 | 290 | self.event_loop_running = false; 291 | } 292 | } 293 | 294 | fn mouse_id(id: u8) -> MouseButton { 295 | match id { 296 | 1 => MouseButton::Left, 297 | 2 => MouseButton::Middle, 298 | 3 => MouseButton::Right, 299 | 8 => MouseButton::Back, 300 | 9 => MouseButton::Forward, 301 | id => MouseButton::Other(id), 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/x11/keyboard.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Druid Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Baseview modifications to druid code: 16 | // - collect functions from various files 17 | // - update imports, paths etc 18 | 19 | //! X11 keyboard handling 20 | 21 | use x11rb::protocol::xproto::{KeyButMask, KeyPressEvent, KeyReleaseEvent}; 22 | 23 | use keyboard_types::*; 24 | 25 | use crate::keyboard::code_to_location; 26 | 27 | /// Convert a hardware scan code to a key. 28 | /// 29 | /// Note: this is a hardcoded layout. We need to detect the user's 30 | /// layout from the system and apply it. 31 | fn code_to_key(code: Code, m: Modifiers) -> Key { 32 | fn a(s: &str) -> Key { 33 | Key::Character(s.into()) 34 | } 35 | fn s(mods: Modifiers, base: &str, shifted: &str) -> Key { 36 | if mods.contains(Modifiers::SHIFT) { 37 | Key::Character(shifted.into()) 38 | } else { 39 | Key::Character(base.into()) 40 | } 41 | } 42 | fn n(mods: Modifiers, base: Key, num: &str) -> Key { 43 | if mods.contains(Modifiers::NUM_LOCK) != mods.contains(Modifiers::SHIFT) { 44 | Key::Character(num.into()) 45 | } else { 46 | base 47 | } 48 | } 49 | match code { 50 | Code::KeyA => s(m, "a", "A"), 51 | Code::KeyB => s(m, "b", "B"), 52 | Code::KeyC => s(m, "c", "C"), 53 | Code::KeyD => s(m, "d", "D"), 54 | Code::KeyE => s(m, "e", "E"), 55 | Code::KeyF => s(m, "f", "F"), 56 | Code::KeyG => s(m, "g", "G"), 57 | Code::KeyH => s(m, "h", "H"), 58 | Code::KeyI => s(m, "i", "I"), 59 | Code::KeyJ => s(m, "j", "J"), 60 | Code::KeyK => s(m, "k", "K"), 61 | Code::KeyL => s(m, "l", "L"), 62 | Code::KeyM => s(m, "m", "M"), 63 | Code::KeyN => s(m, "n", "N"), 64 | Code::KeyO => s(m, "o", "O"), 65 | Code::KeyP => s(m, "p", "P"), 66 | Code::KeyQ => s(m, "q", "Q"), 67 | Code::KeyR => s(m, "r", "R"), 68 | Code::KeyS => s(m, "s", "S"), 69 | Code::KeyT => s(m, "t", "T"), 70 | Code::KeyU => s(m, "u", "U"), 71 | Code::KeyV => s(m, "v", "V"), 72 | Code::KeyW => s(m, "w", "W"), 73 | Code::KeyX => s(m, "x", "X"), 74 | Code::KeyY => s(m, "y", "Y"), 75 | Code::KeyZ => s(m, "z", "Z"), 76 | 77 | Code::Digit0 => s(m, "0", ")"), 78 | Code::Digit1 => s(m, "1", "!"), 79 | Code::Digit2 => s(m, "2", "@"), 80 | Code::Digit3 => s(m, "3", "#"), 81 | Code::Digit4 => s(m, "4", "$"), 82 | Code::Digit5 => s(m, "5", "%"), 83 | Code::Digit6 => s(m, "6", "^"), 84 | Code::Digit7 => s(m, "7", "&"), 85 | Code::Digit8 => s(m, "8", "*"), 86 | Code::Digit9 => s(m, "9", "("), 87 | 88 | Code::Backquote => s(m, "`", "~"), 89 | Code::Minus => s(m, "-", "_"), 90 | Code::Equal => s(m, "=", "+"), 91 | Code::BracketLeft => s(m, "[", "{"), 92 | Code::BracketRight => s(m, "]", "}"), 93 | Code::Backslash => s(m, "\\", "|"), 94 | Code::Semicolon => s(m, ";", ":"), 95 | Code::Quote => s(m, "'", "\""), 96 | Code::Comma => s(m, ",", "<"), 97 | Code::Period => s(m, ".", ">"), 98 | Code::Slash => s(m, "/", "?"), 99 | 100 | Code::Space => a(" "), 101 | 102 | Code::Escape => Key::Escape, 103 | Code::Backspace => Key::Backspace, 104 | Code::Tab => Key::Tab, 105 | Code::Enter => Key::Enter, 106 | Code::ControlLeft => Key::Control, 107 | Code::ShiftLeft => Key::Shift, 108 | Code::ShiftRight => Key::Shift, 109 | Code::NumpadMultiply => a("*"), 110 | Code::AltLeft => Key::Alt, 111 | Code::CapsLock => Key::CapsLock, 112 | Code::F1 => Key::F1, 113 | Code::F2 => Key::F2, 114 | Code::F3 => Key::F3, 115 | Code::F4 => Key::F4, 116 | Code::F5 => Key::F5, 117 | Code::F6 => Key::F6, 118 | Code::F7 => Key::F7, 119 | Code::F8 => Key::F8, 120 | Code::F9 => Key::F9, 121 | Code::F10 => Key::F10, 122 | Code::NumLock => Key::NumLock, 123 | Code::ScrollLock => Key::ScrollLock, 124 | Code::Numpad0 => n(m, Key::Insert, "0"), 125 | Code::Numpad1 => n(m, Key::End, "1"), 126 | Code::Numpad2 => n(m, Key::ArrowDown, "2"), 127 | Code::Numpad3 => n(m, Key::PageDown, "3"), 128 | Code::Numpad4 => n(m, Key::ArrowLeft, "4"), 129 | Code::Numpad5 => n(m, Key::Clear, "5"), 130 | Code::Numpad6 => n(m, Key::ArrowRight, "6"), 131 | Code::Numpad7 => n(m, Key::Home, "7"), 132 | Code::Numpad8 => n(m, Key::ArrowUp, "8"), 133 | Code::Numpad9 => n(m, Key::PageUp, "9"), 134 | Code::NumpadSubtract => a("-"), 135 | Code::NumpadAdd => a("+"), 136 | Code::NumpadDecimal => n(m, Key::Delete, "."), 137 | Code::IntlBackslash => s(m, "\\", "|"), 138 | Code::F11 => Key::F11, 139 | Code::F12 => Key::F12, 140 | // This mapping is based on the picture in the w3c spec. 141 | Code::IntlRo => a("\\"), 142 | Code::Convert => Key::Convert, 143 | Code::KanaMode => Key::KanaMode, 144 | Code::NonConvert => Key::NonConvert, 145 | Code::NumpadEnter => Key::Enter, 146 | Code::ControlRight => Key::Control, 147 | Code::NumpadDivide => a("/"), 148 | Code::PrintScreen => Key::PrintScreen, 149 | Code::AltRight => Key::Alt, 150 | Code::Home => Key::Home, 151 | Code::ArrowUp => Key::ArrowUp, 152 | Code::PageUp => Key::PageUp, 153 | Code::ArrowLeft => Key::ArrowLeft, 154 | Code::ArrowRight => Key::ArrowRight, 155 | Code::End => Key::End, 156 | Code::ArrowDown => Key::ArrowDown, 157 | Code::PageDown => Key::PageDown, 158 | Code::Insert => Key::Insert, 159 | Code::Delete => Key::Delete, 160 | Code::AudioVolumeMute => Key::AudioVolumeMute, 161 | Code::AudioVolumeDown => Key::AudioVolumeDown, 162 | Code::AudioVolumeUp => Key::AudioVolumeUp, 163 | Code::NumpadEqual => a("="), 164 | Code::Pause => Key::Pause, 165 | Code::NumpadComma => a(","), 166 | Code::Lang1 => Key::HangulMode, 167 | Code::Lang2 => Key::HanjaMode, 168 | Code::IntlYen => a("¥"), 169 | Code::MetaLeft => Key::Meta, 170 | Code::MetaRight => Key::Meta, 171 | Code::ContextMenu => Key::ContextMenu, 172 | Code::BrowserStop => Key::BrowserStop, 173 | Code::Again => Key::Again, 174 | Code::Props => Key::Props, 175 | Code::Undo => Key::Undo, 176 | Code::Select => Key::Select, 177 | Code::Copy => Key::Copy, 178 | Code::Open => Key::Open, 179 | Code::Paste => Key::Paste, 180 | Code::Find => Key::Find, 181 | Code::Cut => Key::Cut, 182 | Code::Help => Key::Help, 183 | Code::LaunchApp2 => Key::LaunchApplication2, 184 | Code::WakeUp => Key::WakeUp, 185 | Code::LaunchApp1 => Key::LaunchApplication1, 186 | Code::LaunchMail => Key::LaunchMail, 187 | Code::BrowserFavorites => Key::BrowserFavorites, 188 | Code::BrowserBack => Key::BrowserBack, 189 | Code::BrowserForward => Key::BrowserForward, 190 | Code::Eject => Key::Eject, 191 | Code::MediaTrackNext => Key::MediaTrackNext, 192 | Code::MediaPlayPause => Key::MediaPlayPause, 193 | Code::MediaTrackPrevious => Key::MediaTrackPrevious, 194 | Code::MediaStop => Key::MediaStop, 195 | Code::MediaSelect => Key::LaunchMediaPlayer, 196 | Code::BrowserHome => Key::BrowserHome, 197 | Code::BrowserRefresh => Key::BrowserRefresh, 198 | Code::BrowserSearch => Key::BrowserSearch, 199 | 200 | _ => Key::Unidentified, 201 | } 202 | } 203 | 204 | #[cfg(target_os = "linux")] 205 | /// Map hardware keycode to code. 206 | /// 207 | /// In theory, the hardware keycode is device dependent, but in 208 | /// practice it's probably pretty reliable. 209 | /// 210 | /// The logic is based on NativeKeyToDOMCodeName.h in Mozilla. 211 | fn hardware_keycode_to_code(hw_keycode: u16) -> Code { 212 | match hw_keycode { 213 | 0x0009 => Code::Escape, 214 | 0x000A => Code::Digit1, 215 | 0x000B => Code::Digit2, 216 | 0x000C => Code::Digit3, 217 | 0x000D => Code::Digit4, 218 | 0x000E => Code::Digit5, 219 | 0x000F => Code::Digit6, 220 | 0x0010 => Code::Digit7, 221 | 0x0011 => Code::Digit8, 222 | 0x0012 => Code::Digit9, 223 | 0x0013 => Code::Digit0, 224 | 0x0014 => Code::Minus, 225 | 0x0015 => Code::Equal, 226 | 0x0016 => Code::Backspace, 227 | 0x0017 => Code::Tab, 228 | 0x0018 => Code::KeyQ, 229 | 0x0019 => Code::KeyW, 230 | 0x001A => Code::KeyE, 231 | 0x001B => Code::KeyR, 232 | 0x001C => Code::KeyT, 233 | 0x001D => Code::KeyY, 234 | 0x001E => Code::KeyU, 235 | 0x001F => Code::KeyI, 236 | 0x0020 => Code::KeyO, 237 | 0x0021 => Code::KeyP, 238 | 0x0022 => Code::BracketLeft, 239 | 0x0023 => Code::BracketRight, 240 | 0x0024 => Code::Enter, 241 | 0x0025 => Code::ControlLeft, 242 | 0x0026 => Code::KeyA, 243 | 0x0027 => Code::KeyS, 244 | 0x0028 => Code::KeyD, 245 | 0x0029 => Code::KeyF, 246 | 0x002A => Code::KeyG, 247 | 0x002B => Code::KeyH, 248 | 0x002C => Code::KeyJ, 249 | 0x002D => Code::KeyK, 250 | 0x002E => Code::KeyL, 251 | 0x002F => Code::Semicolon, 252 | 0x0030 => Code::Quote, 253 | 0x0031 => Code::Backquote, 254 | 0x0032 => Code::ShiftLeft, 255 | 0x0033 => Code::Backslash, 256 | 0x0034 => Code::KeyZ, 257 | 0x0035 => Code::KeyX, 258 | 0x0036 => Code::KeyC, 259 | 0x0037 => Code::KeyV, 260 | 0x0038 => Code::KeyB, 261 | 0x0039 => Code::KeyN, 262 | 0x003A => Code::KeyM, 263 | 0x003B => Code::Comma, 264 | 0x003C => Code::Period, 265 | 0x003D => Code::Slash, 266 | 0x003E => Code::ShiftRight, 267 | 0x003F => Code::NumpadMultiply, 268 | 0x0040 => Code::AltLeft, 269 | 0x0041 => Code::Space, 270 | 0x0042 => Code::CapsLock, 271 | 0x0043 => Code::F1, 272 | 0x0044 => Code::F2, 273 | 0x0045 => Code::F3, 274 | 0x0046 => Code::F4, 275 | 0x0047 => Code::F5, 276 | 0x0048 => Code::F6, 277 | 0x0049 => Code::F7, 278 | 0x004A => Code::F8, 279 | 0x004B => Code::F9, 280 | 0x004C => Code::F10, 281 | 0x004D => Code::NumLock, 282 | 0x004E => Code::ScrollLock, 283 | 0x004F => Code::Numpad7, 284 | 0x0050 => Code::Numpad8, 285 | 0x0051 => Code::Numpad9, 286 | 0x0052 => Code::NumpadSubtract, 287 | 0x0053 => Code::Numpad4, 288 | 0x0054 => Code::Numpad5, 289 | 0x0055 => Code::Numpad6, 290 | 0x0056 => Code::NumpadAdd, 291 | 0x0057 => Code::Numpad1, 292 | 0x0058 => Code::Numpad2, 293 | 0x0059 => Code::Numpad3, 294 | 0x005A => Code::Numpad0, 295 | 0x005B => Code::NumpadDecimal, 296 | 0x005E => Code::IntlBackslash, 297 | 0x005F => Code::F11, 298 | 0x0060 => Code::F12, 299 | 0x0061 => Code::IntlRo, 300 | 0x0064 => Code::Convert, 301 | 0x0065 => Code::KanaMode, 302 | 0x0066 => Code::NonConvert, 303 | 0x0068 => Code::NumpadEnter, 304 | 0x0069 => Code::ControlRight, 305 | 0x006A => Code::NumpadDivide, 306 | 0x006B => Code::PrintScreen, 307 | 0x006C => Code::AltRight, 308 | 0x006E => Code::Home, 309 | 0x006F => Code::ArrowUp, 310 | 0x0070 => Code::PageUp, 311 | 0x0071 => Code::ArrowLeft, 312 | 0x0072 => Code::ArrowRight, 313 | 0x0073 => Code::End, 314 | 0x0074 => Code::ArrowDown, 315 | 0x0075 => Code::PageDown, 316 | 0x0076 => Code::Insert, 317 | 0x0077 => Code::Delete, 318 | 0x0079 => Code::AudioVolumeMute, 319 | 0x007A => Code::AudioVolumeDown, 320 | 0x007B => Code::AudioVolumeUp, 321 | 0x007D => Code::NumpadEqual, 322 | 0x007F => Code::Pause, 323 | 0x0081 => Code::NumpadComma, 324 | 0x0082 => Code::Lang1, 325 | 0x0083 => Code::Lang2, 326 | 0x0084 => Code::IntlYen, 327 | 0x0085 => Code::MetaLeft, 328 | 0x0086 => Code::MetaRight, 329 | 0x0087 => Code::ContextMenu, 330 | 0x0088 => Code::BrowserStop, 331 | 0x0089 => Code::Again, 332 | 0x008A => Code::Props, 333 | 0x008B => Code::Undo, 334 | 0x008C => Code::Select, 335 | 0x008D => Code::Copy, 336 | 0x008E => Code::Open, 337 | 0x008F => Code::Paste, 338 | 0x0090 => Code::Find, 339 | 0x0091 => Code::Cut, 340 | 0x0092 => Code::Help, 341 | 0x0094 => Code::LaunchApp2, 342 | 0x0097 => Code::WakeUp, 343 | 0x0098 => Code::LaunchApp1, 344 | // key to right of volume controls on T430s produces 0x9C 345 | // but no documentation of what it should map to :/ 346 | 0x00A3 => Code::LaunchMail, 347 | 0x00A4 => Code::BrowserFavorites, 348 | 0x00A6 => Code::BrowserBack, 349 | 0x00A7 => Code::BrowserForward, 350 | 0x00A9 => Code::Eject, 351 | 0x00AB => Code::MediaTrackNext, 352 | 0x00AC => Code::MediaPlayPause, 353 | 0x00AD => Code::MediaTrackPrevious, 354 | 0x00AE => Code::MediaStop, 355 | 0x00B3 => Code::MediaSelect, 356 | 0x00B4 => Code::BrowserHome, 357 | 0x00B5 => Code::BrowserRefresh, 358 | 0x00E1 => Code::BrowserSearch, 359 | _ => Code::Unidentified, 360 | } 361 | } 362 | 363 | // Extracts the keyboard modifiers from, e.g., the `state` field of 364 | // `x11rb::protocol::xproto::ButtonPressEvent` 365 | pub(super) fn key_mods(mods: KeyButMask) -> Modifiers { 366 | let mut ret = Modifiers::default(); 367 | let key_masks = [ 368 | (KeyButMask::SHIFT, Modifiers::SHIFT), 369 | (KeyButMask::CONTROL, Modifiers::CONTROL), 370 | // X11's mod keys are configurable, but this seems 371 | // like a reasonable default for US keyboards, at least, 372 | // where the "windows" key seems to be MOD_MASK_4. 373 | (KeyButMask::BUTTON1, Modifiers::ALT), 374 | (KeyButMask::BUTTON2, Modifiers::NUM_LOCK), 375 | (KeyButMask::BUTTON4, Modifiers::META), 376 | (KeyButMask::LOCK, Modifiers::CAPS_LOCK), 377 | ]; 378 | for (mask, modifiers) in &key_masks { 379 | if mods.contains(*mask) { 380 | ret |= *modifiers; 381 | } 382 | } 383 | ret 384 | } 385 | 386 | pub(super) fn convert_key_press_event(key_press: &KeyPressEvent) -> KeyboardEvent { 387 | let hw_keycode = key_press.detail; 388 | let code = hardware_keycode_to_code(hw_keycode.into()); 389 | let modifiers = key_mods(key_press.state); 390 | let key = code_to_key(code, modifiers); 391 | let location = code_to_location(code); 392 | let state = KeyState::Down; 393 | 394 | KeyboardEvent { code, key, modifiers, location, state, repeat: false, is_composing: false } 395 | } 396 | 397 | pub(super) fn convert_key_release_event(key_release: &KeyReleaseEvent) -> KeyboardEvent { 398 | let hw_keycode = key_release.detail; 399 | let code = hardware_keycode_to_code(hw_keycode.into()); 400 | let modifiers = key_mods(key_release.state); 401 | let key = code_to_key(code, modifiers); 402 | let location = code_to_location(code); 403 | let state = KeyState::Up; 404 | 405 | KeyboardEvent { code, key, modifiers, location, state, repeat: false, is_composing: false } 406 | } 407 | -------------------------------------------------------------------------------- /src/x11/mod.rs: -------------------------------------------------------------------------------- 1 | mod xcb_connection; 2 | use xcb_connection::XcbConnection; 3 | 4 | mod window; 5 | pub use window::*; 6 | 7 | mod cursor; 8 | mod event_loop; 9 | mod keyboard; 10 | mod visual_info; 11 | -------------------------------------------------------------------------------- /src/x11/visual_info.rs: -------------------------------------------------------------------------------- 1 | use crate::x11::xcb_connection::XcbConnection; 2 | use std::error::Error; 3 | use x11rb::connection::Connection; 4 | use x11rb::protocol::xproto::{ 5 | Colormap, ColormapAlloc, ConnectionExt, Screen, VisualClass, Visualid, 6 | }; 7 | use x11rb::COPY_FROM_PARENT; 8 | 9 | pub(super) struct WindowVisualConfig { 10 | #[cfg(feature = "opengl")] 11 | pub fb_config: Option, 12 | 13 | pub visual_depth: u8, 14 | pub visual_id: Visualid, 15 | pub color_map: Option, 16 | } 17 | 18 | // TODO: make visual negotiation actually check all of a visual's parameters 19 | impl WindowVisualConfig { 20 | #[cfg(feature = "opengl")] 21 | pub fn find_best_visual_config_for_gl( 22 | connection: &XcbConnection, gl_config: Option, 23 | ) -> Result> { 24 | let Some(gl_config) = gl_config else { return Self::find_best_visual_config(connection) }; 25 | 26 | // SAFETY: TODO 27 | let (fb_config, window_config) = unsafe { 28 | crate::gl::platform::GlContext::get_fb_config_and_visual(connection.dpy, gl_config) 29 | } 30 | .expect("Could not fetch framebuffer config"); 31 | 32 | Ok(Self { 33 | fb_config: Some(fb_config), 34 | visual_depth: window_config.depth, 35 | visual_id: window_config.visual, 36 | color_map: Some(create_color_map(connection, window_config.visual)?), 37 | }) 38 | } 39 | 40 | pub fn find_best_visual_config(connection: &XcbConnection) -> Result> { 41 | match find_visual_for_depth(connection.screen(), 32) { 42 | None => Ok(Self::copy_from_parent()), 43 | Some(visual_id) => Ok(Self { 44 | #[cfg(feature = "opengl")] 45 | fb_config: None, 46 | visual_id, 47 | visual_depth: 32, 48 | color_map: Some(create_color_map(connection, visual_id)?), 49 | }), 50 | } 51 | } 52 | 53 | const fn copy_from_parent() -> Self { 54 | Self { 55 | #[cfg(feature = "opengl")] 56 | fb_config: None, 57 | visual_depth: COPY_FROM_PARENT as u8, 58 | visual_id: COPY_FROM_PARENT, 59 | color_map: None, 60 | } 61 | } 62 | } 63 | 64 | // For this 32-bit depth to work, you also need to define a color map and set a border 65 | // pixel: https://cgit.freedesktop.org/xorg/xserver/tree/dix/window.c#n818 66 | fn create_color_map( 67 | connection: &XcbConnection, visual_id: Visualid, 68 | ) -> Result> { 69 | let colormap = connection.conn.generate_id()?; 70 | connection.conn.create_colormap( 71 | ColormapAlloc::NONE, 72 | colormap, 73 | connection.screen().root, 74 | visual_id, 75 | )?; 76 | 77 | Ok(colormap) 78 | } 79 | 80 | fn find_visual_for_depth(screen: &Screen, depth: u8) -> Option { 81 | for candidate_depth in &screen.allowed_depths { 82 | if candidate_depth.depth != depth { 83 | continue; 84 | } 85 | 86 | for candidate_visual in &candidate_depth.visuals { 87 | if candidate_visual.class == VisualClass::TRUE_COLOR { 88 | return Some(candidate_visual.visual_id); 89 | } 90 | } 91 | } 92 | 93 | None 94 | } 95 | -------------------------------------------------------------------------------- /src/x11/window.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | use std::error::Error; 3 | use std::ffi::c_void; 4 | use std::sync::atomic::{AtomicBool, Ordering}; 5 | use std::sync::mpsc; 6 | use std::sync::Arc; 7 | use std::thread; 8 | 9 | use raw_window_handle::{ 10 | HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, XlibDisplayHandle, 11 | XlibWindowHandle, 12 | }; 13 | 14 | use x11rb::connection::Connection; 15 | use x11rb::protocol::xproto::{ 16 | AtomEnum, ChangeWindowAttributesAux, ConfigureWindowAux, ConnectionExt as _, CreateGCAux, 17 | CreateWindowAux, EventMask, PropMode, Visualid, Window as XWindow, WindowClass, 18 | }; 19 | use x11rb::wrapper::ConnectionExt as _; 20 | 21 | use super::XcbConnection; 22 | use crate::{ 23 | Event, MouseCursor, Size, WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions, 24 | WindowScalePolicy, 25 | }; 26 | 27 | #[cfg(feature = "opengl")] 28 | use crate::gl::{platform, GlContext}; 29 | use crate::x11::event_loop::EventLoop; 30 | use crate::x11::visual_info::WindowVisualConfig; 31 | 32 | pub struct WindowHandle { 33 | raw_window_handle: Option, 34 | close_requested: Arc, 35 | is_open: Arc, 36 | } 37 | 38 | impl WindowHandle { 39 | pub fn close(&mut self) { 40 | if self.raw_window_handle.take().is_some() { 41 | // FIXME: This will need to be changed from just setting an atomic to somehow 42 | // synchronizing with the window being closed (using a synchronous channel, or 43 | // by joining on the event loop thread). 44 | 45 | self.close_requested.store(true, Ordering::Relaxed); 46 | } 47 | } 48 | 49 | pub fn is_open(&self) -> bool { 50 | self.is_open.load(Ordering::Relaxed) 51 | } 52 | } 53 | 54 | unsafe impl HasRawWindowHandle for WindowHandle { 55 | fn raw_window_handle(&self) -> RawWindowHandle { 56 | if let Some(raw_window_handle) = self.raw_window_handle { 57 | if self.is_open.load(Ordering::Relaxed) { 58 | return raw_window_handle; 59 | } 60 | } 61 | 62 | RawWindowHandle::Xlib(XlibWindowHandle::empty()) 63 | } 64 | } 65 | 66 | pub(crate) struct ParentHandle { 67 | close_requested: Arc, 68 | is_open: Arc, 69 | } 70 | 71 | impl ParentHandle { 72 | pub fn new() -> (Self, WindowHandle) { 73 | let close_requested = Arc::new(AtomicBool::new(false)); 74 | let is_open = Arc::new(AtomicBool::new(true)); 75 | 76 | let handle = WindowHandle { 77 | raw_window_handle: None, 78 | close_requested: Arc::clone(&close_requested), 79 | is_open: Arc::clone(&is_open), 80 | }; 81 | 82 | (Self { close_requested, is_open }, handle) 83 | } 84 | 85 | pub fn parent_did_drop(&self) -> bool { 86 | self.close_requested.load(Ordering::Relaxed) 87 | } 88 | } 89 | 90 | impl Drop for ParentHandle { 91 | fn drop(&mut self) { 92 | self.is_open.store(false, Ordering::Relaxed); 93 | } 94 | } 95 | 96 | pub(crate) struct WindowInner { 97 | pub(crate) xcb_connection: XcbConnection, 98 | window_id: XWindow, 99 | pub(crate) window_info: WindowInfo, 100 | visual_id: Visualid, 101 | mouse_cursor: Cell, 102 | 103 | pub(crate) close_requested: Cell, 104 | 105 | #[cfg(feature = "opengl")] 106 | gl_context: Option, 107 | } 108 | 109 | pub struct Window<'a> { 110 | pub(crate) inner: &'a WindowInner, 111 | } 112 | 113 | // Hack to allow sending a RawWindowHandle between threads. Do not make public 114 | struct SendableRwh(RawWindowHandle); 115 | 116 | unsafe impl Send for SendableRwh {} 117 | 118 | type WindowOpenResult = Result; 119 | 120 | impl<'a> Window<'a> { 121 | pub fn open_parented(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle 122 | where 123 | P: HasRawWindowHandle, 124 | H: WindowHandler + 'static, 125 | B: FnOnce(&mut crate::Window) -> H, 126 | B: Send + 'static, 127 | { 128 | // Convert parent into something that X understands 129 | let parent_id = match parent.raw_window_handle() { 130 | RawWindowHandle::Xlib(h) => h.window as u32, 131 | RawWindowHandle::Xcb(h) => h.window, 132 | h => panic!("unsupported parent handle type {:?}", h), 133 | }; 134 | 135 | let (tx, rx) = mpsc::sync_channel::(1); 136 | 137 | let (parent_handle, mut window_handle) = ParentHandle::new(); 138 | 139 | thread::spawn(move || { 140 | Self::window_thread(Some(parent_id), options, build, tx.clone(), Some(parent_handle)) 141 | .unwrap(); 142 | }); 143 | 144 | let raw_window_handle = rx.recv().unwrap().unwrap(); 145 | window_handle.raw_window_handle = Some(raw_window_handle.0); 146 | 147 | window_handle 148 | } 149 | 150 | pub fn open_blocking(options: WindowOpenOptions, build: B) 151 | where 152 | H: WindowHandler + 'static, 153 | B: FnOnce(&mut crate::Window) -> H, 154 | B: Send + 'static, 155 | { 156 | let (tx, rx) = mpsc::sync_channel::(1); 157 | 158 | let thread = thread::spawn(move || { 159 | Self::window_thread(None, options, build, tx, None).unwrap(); 160 | }); 161 | 162 | let _ = rx.recv().unwrap().unwrap(); 163 | 164 | thread.join().unwrap_or_else(|err| { 165 | eprintln!("Window thread panicked: {:#?}", err); 166 | }); 167 | } 168 | 169 | fn window_thread( 170 | parent: Option, options: WindowOpenOptions, build: B, 171 | tx: mpsc::SyncSender, parent_handle: Option, 172 | ) -> Result<(), Box> 173 | where 174 | H: WindowHandler + 'static, 175 | B: FnOnce(&mut crate::Window) -> H, 176 | B: Send + 'static, 177 | { 178 | // Connect to the X server 179 | // FIXME: baseview error type instead of unwrap() 180 | let xcb_connection = XcbConnection::new()?; 181 | 182 | // Get screen information 183 | let screen = xcb_connection.screen(); 184 | let parent_id = parent.unwrap_or(screen.root); 185 | 186 | let gc_id = xcb_connection.conn.generate_id()?; 187 | xcb_connection.conn.create_gc( 188 | gc_id, 189 | parent_id, 190 | &CreateGCAux::new().foreground(screen.black_pixel).graphics_exposures(0), 191 | )?; 192 | 193 | let scaling = match options.scale { 194 | WindowScalePolicy::SystemScaleFactor => xcb_connection.get_scaling().unwrap_or(1.0), 195 | WindowScalePolicy::ScaleFactor(scale) => scale, 196 | }; 197 | 198 | let window_info = WindowInfo::from_logical_size(options.size, scaling); 199 | 200 | #[cfg(feature = "opengl")] 201 | let visual_info = 202 | WindowVisualConfig::find_best_visual_config_for_gl(&xcb_connection, options.gl_config)?; 203 | 204 | #[cfg(not(feature = "opengl"))] 205 | let visual_info = WindowVisualConfig::find_best_visual_config(&xcb_connection)?; 206 | 207 | let window_id = xcb_connection.conn.generate_id()?; 208 | xcb_connection.conn.create_window( 209 | visual_info.visual_depth, 210 | window_id, 211 | parent_id, 212 | 0, // x coordinate of the new window 213 | 0, // y coordinate of the new window 214 | window_info.physical_size().width as u16, // window width 215 | window_info.physical_size().height as u16, // window height 216 | 0, // window border 217 | WindowClass::INPUT_OUTPUT, 218 | visual_info.visual_id, 219 | &CreateWindowAux::new() 220 | .event_mask( 221 | EventMask::EXPOSURE 222 | | EventMask::POINTER_MOTION 223 | | EventMask::BUTTON_PRESS 224 | | EventMask::BUTTON_RELEASE 225 | | EventMask::KEY_PRESS 226 | | EventMask::KEY_RELEASE 227 | | EventMask::STRUCTURE_NOTIFY 228 | | EventMask::ENTER_WINDOW 229 | | EventMask::LEAVE_WINDOW, 230 | ) 231 | // As mentioned above, these two values are needed to be able to create a window 232 | // with a depth of 32-bits when the parent window has a different depth 233 | .colormap(visual_info.color_map) 234 | .border_pixel(0), 235 | )?; 236 | xcb_connection.conn.map_window(window_id)?; 237 | 238 | // Change window title 239 | let title = options.title; 240 | xcb_connection.conn.change_property8( 241 | PropMode::REPLACE, 242 | window_id, 243 | AtomEnum::WM_NAME, 244 | AtomEnum::STRING, 245 | title.as_bytes(), 246 | )?; 247 | 248 | xcb_connection.conn.change_property32( 249 | PropMode::REPLACE, 250 | window_id, 251 | xcb_connection.atoms.WM_PROTOCOLS, 252 | AtomEnum::ATOM, 253 | &[xcb_connection.atoms.WM_DELETE_WINDOW], 254 | )?; 255 | 256 | xcb_connection.conn.flush()?; 257 | 258 | // TODO: These APIs could use a couple tweaks now that everything is internal and there is 259 | // no error handling anymore at this point. Everything is more or less unchanged 260 | // compared to when raw-gl-context was a separate crate. 261 | #[cfg(feature = "opengl")] 262 | let gl_context = visual_info.fb_config.map(|fb_config| { 263 | use std::ffi::c_ulong; 264 | 265 | let window = window_id as c_ulong; 266 | let display = xcb_connection.dpy; 267 | 268 | // Because of the visual negotation we had to take some extra steps to create this context 269 | let context = unsafe { platform::GlContext::create(window, display, fb_config) } 270 | .expect("Could not create OpenGL context"); 271 | GlContext::new(context) 272 | }); 273 | 274 | let mut inner = WindowInner { 275 | xcb_connection, 276 | window_id, 277 | window_info, 278 | visual_id: visual_info.visual_id, 279 | mouse_cursor: Cell::new(MouseCursor::default()), 280 | 281 | close_requested: Cell::new(false), 282 | 283 | #[cfg(feature = "opengl")] 284 | gl_context, 285 | }; 286 | 287 | let mut window = crate::Window::new(Window { inner: &mut inner }); 288 | 289 | let mut handler = build(&mut window); 290 | 291 | // Send an initial window resized event so the user is alerted of 292 | // the correct dpi scaling. 293 | handler.on_event(&mut window, Event::Window(WindowEvent::Resized(window_info))); 294 | 295 | let _ = tx.send(Ok(SendableRwh(window.raw_window_handle()))); 296 | 297 | EventLoop::new(inner, handler, parent_handle).run()?; 298 | 299 | Ok(()) 300 | } 301 | 302 | pub fn set_mouse_cursor(&self, mouse_cursor: MouseCursor) { 303 | if self.inner.mouse_cursor.get() == mouse_cursor { 304 | return; 305 | } 306 | 307 | let xid = self.inner.xcb_connection.get_cursor(mouse_cursor).unwrap(); 308 | 309 | if xid != 0 { 310 | let _ = self.inner.xcb_connection.conn.change_window_attributes( 311 | self.inner.window_id, 312 | &ChangeWindowAttributesAux::new().cursor(xid), 313 | ); 314 | let _ = self.inner.xcb_connection.conn.flush(); 315 | } 316 | 317 | self.inner.mouse_cursor.set(mouse_cursor); 318 | } 319 | 320 | pub fn close(&mut self) { 321 | self.inner.close_requested.set(true); 322 | } 323 | 324 | pub fn has_focus(&mut self) -> bool { 325 | unimplemented!() 326 | } 327 | 328 | pub fn focus(&mut self) { 329 | unimplemented!() 330 | } 331 | 332 | pub fn resize(&mut self, size: Size) { 333 | let scaling = self.inner.window_info.scale(); 334 | let new_window_info = WindowInfo::from_logical_size(size, scaling); 335 | 336 | let _ = self.inner.xcb_connection.conn.configure_window( 337 | self.inner.window_id, 338 | &ConfigureWindowAux::new() 339 | .width(new_window_info.physical_size().width) 340 | .height(new_window_info.physical_size().height), 341 | ); 342 | let _ = self.inner.xcb_connection.conn.flush(); 343 | 344 | // This will trigger a `ConfigureNotify` event which will in turn change `self.window_info` 345 | // and notify the window handler about it 346 | } 347 | 348 | #[cfg(feature = "opengl")] 349 | pub fn gl_context(&self) -> Option<&crate::gl::GlContext> { 350 | self.inner.gl_context.as_ref() 351 | } 352 | } 353 | 354 | unsafe impl<'a> HasRawWindowHandle for Window<'a> { 355 | fn raw_window_handle(&self) -> RawWindowHandle { 356 | let mut handle = XlibWindowHandle::empty(); 357 | 358 | handle.window = self.inner.window_id.into(); 359 | handle.visual_id = self.inner.visual_id.into(); 360 | 361 | RawWindowHandle::Xlib(handle) 362 | } 363 | } 364 | 365 | unsafe impl<'a> HasRawDisplayHandle for Window<'a> { 366 | fn raw_display_handle(&self) -> RawDisplayHandle { 367 | let display = self.inner.xcb_connection.dpy; 368 | let mut handle = XlibDisplayHandle::empty(); 369 | 370 | handle.display = display as *mut c_void; 371 | handle.screen = unsafe { x11::xlib::XDefaultScreen(display) }; 372 | 373 | RawDisplayHandle::Xlib(handle) 374 | } 375 | } 376 | 377 | pub fn copy_to_clipboard(_data: &str) { 378 | todo!() 379 | } 380 | -------------------------------------------------------------------------------- /src/x11/xcb_connection.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::hash_map::{Entry, HashMap}; 3 | use std::error::Error; 4 | 5 | use x11::{xlib, xlib::Display, xlib_xcb}; 6 | 7 | use x11rb::connection::Connection; 8 | use x11rb::cursor::Handle as CursorHandle; 9 | use x11rb::protocol::xproto::{Cursor, Screen}; 10 | use x11rb::resource_manager; 11 | use x11rb::xcb_ffi::XCBConnection; 12 | 13 | use crate::MouseCursor; 14 | 15 | use super::cursor; 16 | 17 | x11rb::atom_manager! { 18 | pub Atoms: AtomsCookie { 19 | WM_PROTOCOLS, 20 | WM_DELETE_WINDOW, 21 | } 22 | } 23 | 24 | /// A very light abstraction around the XCB connection. 25 | /// 26 | /// Keeps track of the xcb connection itself and the xlib display ID that was used to connect. 27 | pub struct XcbConnection { 28 | pub(crate) dpy: *mut Display, 29 | pub(crate) conn: XCBConnection, 30 | pub(crate) screen: usize, 31 | pub(crate) atoms: Atoms, 32 | pub(crate) resources: resource_manager::Database, 33 | pub(crate) cursor_handle: CursorHandle, 34 | pub(super) cursor_cache: RefCell>, 35 | } 36 | 37 | impl XcbConnection { 38 | pub fn new() -> Result> { 39 | let dpy = unsafe { xlib::XOpenDisplay(std::ptr::null()) }; 40 | assert!(!dpy.is_null()); 41 | let xcb_connection = unsafe { xlib_xcb::XGetXCBConnection(dpy) }; 42 | assert!(!xcb_connection.is_null()); 43 | let screen = unsafe { xlib::XDefaultScreen(dpy) } as usize; 44 | let conn = unsafe { XCBConnection::from_raw_xcb_connection(xcb_connection, false)? }; 45 | unsafe { 46 | xlib_xcb::XSetEventQueueOwner(dpy, xlib_xcb::XEventQueueOwner::XCBOwnsEventQueue) 47 | }; 48 | 49 | let atoms = Atoms::new(&conn)?.reply()?; 50 | let resources = resource_manager::new_from_default(&conn)?; 51 | let cursor_handle = CursorHandle::new(&conn, screen, &resources)?.reply()?; 52 | 53 | Ok(Self { 54 | dpy, 55 | conn, 56 | screen, 57 | atoms, 58 | resources, 59 | cursor_handle, 60 | cursor_cache: RefCell::new(HashMap::new()), 61 | }) 62 | } 63 | 64 | // Try to get the scaling with this function first. 65 | // If this gives you `None`, fall back to `get_scaling_screen_dimensions`. 66 | // If neither work, I guess just assume 96.0 and don't do any scaling. 67 | fn get_scaling_xft(&self) -> Result, Box> { 68 | if let Some(dpi) = self.resources.get_value::("Xft.dpi", "")? { 69 | Ok(Some(dpi as f64 / 96.0)) 70 | } else { 71 | Ok(None) 72 | } 73 | } 74 | 75 | // Try to get the scaling with `get_scaling_xft` first. 76 | // Only use this function as a fallback. 77 | // If neither work, I guess just assume 96.0 and don't do any scaling. 78 | fn get_scaling_screen_dimensions(&self) -> f64 { 79 | // Figure out screen information 80 | let screen = self.screen(); 81 | 82 | // Get the DPI from the screen struct 83 | // 84 | // there are 2.54 centimeters to an inch; so there are 25.4 millimeters. 85 | // dpi = N pixels / (M millimeters / (25.4 millimeters / 1 inch)) 86 | // = N pixels / (M inch / 25.4) 87 | // = N * 25.4 pixels / M inch 88 | let width_px = screen.width_in_pixels as f64; 89 | let width_mm = screen.width_in_millimeters as f64; 90 | let height_px = screen.height_in_pixels as f64; 91 | let height_mm = screen.height_in_millimeters as f64; 92 | let _xres = width_px * 25.4 / width_mm; 93 | let yres = height_px * 25.4 / height_mm; 94 | 95 | // TODO: choose between `xres` and `yres`? (probably both are the same?) 96 | yres / 96.0 97 | } 98 | 99 | #[inline] 100 | pub fn get_scaling(&self) -> Result> { 101 | Ok(self.get_scaling_xft()?.unwrap_or(self.get_scaling_screen_dimensions())) 102 | } 103 | 104 | #[inline] 105 | pub fn get_cursor(&self, cursor: MouseCursor) -> Result> { 106 | // PANIC: this function is the only point where we access the cache, and we never call 107 | // external functions that may make a reentrant call to this function 108 | let mut cursor_cache = self.cursor_cache.borrow_mut(); 109 | 110 | match cursor_cache.entry(cursor) { 111 | Entry::Occupied(entry) => Ok(*entry.get()), 112 | Entry::Vacant(entry) => { 113 | let cursor = 114 | cursor::get_xcursor(&self.conn, self.screen, &self.cursor_handle, cursor)?; 115 | entry.insert(cursor); 116 | Ok(cursor) 117 | } 118 | } 119 | } 120 | 121 | pub fn screen(&self) -> &Screen { 122 | &self.conn.setup().roots[self.screen] 123 | } 124 | } 125 | 126 | impl Drop for XcbConnection { 127 | fn drop(&mut self) { 128 | unsafe { 129 | xlib::XCloseDisplay(self.dpy); 130 | } 131 | } 132 | } 133 | --------------------------------------------------------------------------------