├── .clang-format ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── app.manifest ├── build.rs ├── examples ├── basic.rs ├── fs.rs ├── gallery.rs ├── net.rs └── widgets.rs ├── manifest.rc ├── rustfmt.toml └── src ├── elm ├── button.rs ├── canvas.rs ├── check_box.rs ├── child.rs ├── collection.rs ├── combo_box.rs ├── edit.rs ├── label.rs ├── layout.rs ├── mod.rs ├── progress.rs ├── radio_button.rs └── window.rs ├── lib.rs ├── runtime ├── gtk.rs ├── mac.rs ├── mod.rs ├── qt.cpp ├── qt.hpp ├── qt.rs └── windows.rs └── ui ├── callback.rs ├── canvas.rs ├── drawing.rs ├── filebox.rs ├── gtk ├── button.rs ├── canvas.rs ├── check_box.rs ├── combo_box.rs ├── edit.rs ├── filebox.rs ├── label.rs ├── mod.rs ├── msgbox.rs ├── progress.rs ├── widget.rs └── window.rs ├── mac ├── button.rs ├── canvas.rs ├── combo_box.rs ├── edit.rs ├── filebox.rs ├── label.rs ├── mod.rs ├── msgbox.rs ├── progress.rs └── window.rs ├── mod.rs ├── msgbox.rs ├── qt ├── button.cpp ├── button.hpp ├── button.rs ├── callback.hpp ├── canvas.cpp ├── canvas.hpp ├── canvas.rs ├── combo_box.cpp ├── combo_box.hpp ├── combo_box.rs ├── edit.cpp ├── edit.hpp ├── edit.rs ├── filebox.cpp ├── filebox.hpp ├── filebox.rs ├── label.cpp ├── label.hpp ├── label.rs ├── mod.rs ├── msgbox.cpp ├── msgbox.hpp ├── msgbox.rs ├── progress.cpp ├── progress.hpp ├── progress.rs ├── widget.cpp ├── widget.hpp ├── widget.rs ├── window.cpp ├── window.hpp └── window.rs ├── window_handle.rs └── windows ├── button.rs ├── canvas.rs ├── check_box.rs ├── combo_box.rs ├── darkmode.rs ├── dpi.rs ├── edit.rs ├── filebox.rs ├── font.rs ├── label.rs ├── mod.rs ├── msgbox.rs ├── progress.rs ├── radio_button.rs └── window.rs /.clang-format: -------------------------------------------------------------------------------- 1 | AccessModifierOffset: -4 2 | IndentWidth: 4 3 | TabWidth: 4 4 | UseTab: Never 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | 4 | .vscode/ 5 | .cargo/ 6 | compile_flags.txt 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "winio" 3 | version = "0.3.4" 4 | edition = "2021" 5 | description = "Single-threaded async GUI runtime based on compio." 6 | license = "MIT" 7 | authors = ["Berrysoft "] 8 | readme = "README.md" 9 | repository = "https://github.com/compio-rs/winio" 10 | categories = ["asynchronous", "gui"] 11 | keywords = ["async", "gui"] 12 | 13 | [dependencies] 14 | compio = "0.14.0" 15 | compio-log = "0.1.0" 16 | 17 | cfg-if = "1" 18 | euclid = "0.22" 19 | futures-util = { version = "0.3", features = ["alloc"] } 20 | futures-channel = "0.3" 21 | image = { version = "0.25", default-features = false } 22 | once_cell = "1" 23 | rgb = "0.8" 24 | slab = "0.4" 25 | scoped-tls = "1.0" 26 | taffy = "0.8" 27 | 28 | [target.'cfg(windows)'.dependencies] 29 | widestring = "1" 30 | windows-sys = { version = "0.59", features = [ 31 | "Win32_Foundation", 32 | "Win32_Globalization", 33 | "Win32_Graphics_Gdi", 34 | "Win32_Graphics_Dwm", 35 | "Win32_Networking_WinSock", 36 | "Win32_Security", 37 | "Win32_Storage_FileSystem", 38 | "Win32_System_IO", 39 | "Win32_System_LibraryLoader", 40 | "Win32_System_SystemServices", 41 | "Win32_System_Threading", 42 | "Win32_System_WindowsProgramming", 43 | "Win32_UI_Accessibility", 44 | "Win32_UI_Controls", 45 | "Win32_UI_HiDpi", 46 | "Win32_UI_Shell", 47 | "Win32_UI_WindowsAndMessaging", 48 | ] } 49 | windows = { version = "0.61", features = [ 50 | "Foundation_Numerics", 51 | "Win32_Foundation", 52 | "Win32_Graphics_Direct2D", 53 | "Win32_Graphics_Direct2D_Common", 54 | "Win32_Graphics_DirectWrite", 55 | "Win32_Graphics_Dxgi_Common", 56 | "Win32_System_Com", 57 | "Win32_UI_Shell", 58 | "Win32_UI_Shell_Common", 59 | ] } 60 | windows-numerics = "0.2" 61 | slim-detours-sys = "0.2" 62 | sync-unsafe-cell = "0.1" 63 | 64 | [target.'cfg(target_os = "macos")'.dependencies] 65 | block2 = "0.6" 66 | objc2 = "0.6" 67 | objc2-core-foundation = { version = "0.3", features = [ 68 | "CFCGTypes", 69 | "CFFileDescriptor", 70 | "CFRunLoop", 71 | ] } 72 | objc2-core-graphics = { version = "0.3", features = ["CGColor", "CGPath"] } 73 | objc2-foundation = { version = "0.3", features = [ 74 | "NSAffineTransform", 75 | "NSAttributedString", 76 | "NSDate", 77 | "NSDictionary", 78 | "NSEnumerator", 79 | "NSNotification", 80 | "NSObjCRuntime", 81 | "NSRunLoop", 82 | "NSString", 83 | "NSThread", 84 | "NSURL", 85 | "NSUserDefaults", 86 | ] } 87 | objc2-app-kit = { version = "0.3", features = [ 88 | "block2", 89 | "objc2-quartz-core", 90 | "objc2-uniform-type-identifiers", 91 | "NSAlert", 92 | "NSApplication", 93 | "NSAttributedString", 94 | "NSBitmapImageRep", 95 | "NSButton", 96 | "NSButtonCell", 97 | "NSCell", 98 | "NSColor", 99 | "NSColorSpace", 100 | "NSComboBox", 101 | "NSControl", 102 | "NSEvent", 103 | "NSFont", 104 | "NSFontDescriptor", 105 | "NSGraphics", 106 | "NSImage", 107 | "NSImageRep", 108 | "NSMatrix", 109 | "NSOpenPanel", 110 | "NSPanel", 111 | "NSProgressIndicator", 112 | "NSResponder", 113 | "NSRunningApplication", 114 | "NSSavePanel", 115 | "NSScreen", 116 | "NSSecureTextField", 117 | "NSStringDrawing", 118 | "NSText", 119 | "NSTextField", 120 | "NSTrackingArea", 121 | "NSView", 122 | "NSWindow", 123 | "NSWindowScripting", 124 | ] } 125 | objc2-uniform-type-identifiers = { version = "0.3", features = ["UTType"] } 126 | objc2-quartz-core = { version = "0.3", features = [ 127 | "objc2-core-graphics", 128 | "CALayer", 129 | "CAGradientLayer", 130 | "CAShapeLayer", 131 | "CATextLayer", 132 | ] } 133 | 134 | [target.'cfg(not(any(windows, target_os = "macos")))'.dependencies] 135 | gtk4 = { version = "0.9", optional = true, features = ["v4_14"] } 136 | pangocairo = { version = "0.20", optional = true } 137 | 138 | cxx = { version = "1.0", optional = true, features = ["c++17"] } 139 | 140 | [dev-dependencies] 141 | compio = { version = "0.14.0", features = ["time", "rustls", "ring"] } 142 | cyper = { version = "0.3.1", default-features = false, features = [ 143 | "rustls", 144 | "http2", 145 | "http3-altsvc", 146 | ] } 147 | 148 | dirs = "6.0" 149 | futures-util = "0.3" 150 | image = { version = "0.25", default-features = false, features = [ 151 | "default-formats", 152 | ] } 153 | tracing-subscriber = "0.3" 154 | 155 | [build-dependencies] 156 | embed-resource = "3" 157 | 158 | cxx-build = { version = "1.0", optional = true } 159 | qt-build-utils = { version = "0.7", optional = true } 160 | 161 | [features] 162 | default = ["qt"] 163 | gtk = ["dep:gtk4", "dep:pangocairo"] 164 | qt = ["dep:cxx", "dep:cxx-build", "dep:qt-build-utils"] 165 | enable_log = ["compio/enable_log", "compio-log/enable_log"] 166 | 167 | nightly = ["compio/nightly", "cyper/nightly"] 168 | objc-static = [ 169 | "objc2/unstable-static-class-inlined", 170 | "objc2/unstable-static-sel-inlined", 171 | "objc2-foundation/unstable-static-nsstring", 172 | ] 173 | 174 | [profile.release] 175 | lto = true 176 | codegen-units = 1 177 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Berrysoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Winio 2 | 3 | Winio is a single-threaded asynchronous GUI runtime. 4 | It is based on [`compio`](https://github.com/compio-rs/compio), and the GUI part is powered by Win32, Qt, GTK and Cocoa. 5 | All IO requests could be issued in the same thread as GUI, without blocking the user interface! 6 | 7 | ## Quick start 8 | 9 | Winio follows ELM-like design, inspired by [`yew`](https://yew.rs/) and [`relm4`](https://relm4.org/). 10 | The application starts with a root `Component`: 11 | 12 | ```rust 13 | struct MainModel { 14 | window: Child, 15 | } 16 | 17 | enum MainMessage { 18 | Close, 19 | } 20 | 21 | impl Component for MainModel { 22 | type Event = (); 23 | type Init = (); 24 | type Message = MainMessage; 25 | type Root = (); 26 | 27 | fn init(_counter: Self::Init, _root: &Self::Root, sender: &ComponentSender) -> Self { 28 | // create & initialize the window 29 | let mut window = Child::::init((), &()); 30 | window.set_text("Basic example"); 31 | window.set_size(Size::new(800.0, 600.0)); 32 | Self { window } 33 | } 34 | 35 | async fn start(&mut self, sender: &ComponentSender) { 36 | // listen to events 37 | self.window 38 | .start(sender, |e| match e { 39 | WindowEvent::Close => Some(MainMessage::Close), 40 | _ => None, 41 | }) 42 | .await; 43 | } 44 | 45 | async fn update(&mut self, message: Self::Message, sender: &ComponentSender) -> bool { 46 | // update the window 47 | self.window.update().await; 48 | // deal with custom messages 49 | match message { 50 | MainMessage::Close => { 51 | // the root component output stops the application 52 | sender.output(()); 53 | // need not to call `render` 54 | false 55 | } 56 | } 57 | } 58 | 59 | fn render(&mut self, _sender: &ComponentSender) { 60 | // adjust layout and draw widgets here 61 | } 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | PerMonitorV2 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rerun-if-changed=build.rs"); 3 | 4 | let target_os = std::env::var("CARGO_CFG_TARGET_OS"); 5 | if target_os.as_deref() == Ok("windows") { 6 | println!("cargo:rerun-if-changed=app.manifest"); 7 | println!("cargo:rerun-if-changed=manifest.rc"); 8 | embed_resource::compile("manifest.rc", embed_resource::NONE) 9 | .manifest_required() 10 | .unwrap(); 11 | } 12 | 13 | #[cfg(feature = "qt")] 14 | if target_os.as_deref() != Ok("windows") && target_os.as_deref() != Ok("macos") { 15 | let qbuild = 16 | qt_build_utils::QtBuild::new(vec!["Core".into(), "Gui".into(), "Widgets".into()]) 17 | .unwrap(); 18 | 19 | let sources = [ 20 | "src/runtime/qt", 21 | "src/ui/qt/widget", 22 | "src/ui/qt/msgbox", 23 | "src/ui/qt/filebox", 24 | "src/ui/qt/window", 25 | "src/ui/qt/button", 26 | "src/ui/qt/canvas", 27 | "src/ui/qt/edit", 28 | "src/ui/qt/label", 29 | "src/ui/qt/progress", 30 | "src/ui/qt/combo_box", 31 | ]; 32 | 33 | for s in sources { 34 | println!("cargo:rerun-if-changed={s}.rs"); 35 | println!("cargo:rerun-if-changed={s}.cpp"); 36 | println!("cargo:rerun-if-changed={s}.hpp"); 37 | } 38 | 39 | let inc = qbuild.include_paths(); 40 | 41 | let mut build = cxx_build::bridges(sources.map(|s| format!("{s}.rs"))); 42 | build 43 | .std("c++17") 44 | .files(sources.map(|s| format!("{s}.cpp"))) 45 | .includes(inc); 46 | if std::env::var("PROFILE").as_deref() == Ok("release") { 47 | build.flag("-flto").compiler("clang++"); 48 | } 49 | qbuild.cargo_link_libraries(&mut build); 50 | build.compile("winio"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use compio::{runtime::spawn, time::interval}; 4 | use compio_log::info; 5 | use winio::{ 6 | App, BrushPen, Canvas, CanvasEvent, Child, Color, ColorTheme, Component, ComponentSender, 7 | CustomButton, DrawingFontBuilder, Grid, HAlign, Layoutable, MessageBox, MessageBoxButton, 8 | MessageBoxResponse, MessageBoxStyle, MouseButton, Point, Rect, Size, SolidColorBrush, VAlign, 9 | Window, WindowEvent, 10 | }; 11 | 12 | fn main() { 13 | #[cfg(feature = "enable_log")] 14 | tracing_subscriber::fmt() 15 | .with_max_level(compio_log::Level::INFO) 16 | .init(); 17 | 18 | App::new().run::(0, &()); 19 | } 20 | 21 | struct MainModel { 22 | window: Child, 23 | canvas: Child, 24 | counter: usize, 25 | } 26 | 27 | #[derive(Debug)] 28 | enum MainMessage { 29 | Tick, 30 | Close, 31 | Redraw, 32 | Mouse(MouseButton), 33 | MouseMove(Point), 34 | } 35 | 36 | impl Component for MainModel { 37 | type Event = (); 38 | type Init = usize; 39 | type Message = MainMessage; 40 | type Root = (); 41 | 42 | fn init(counter: Self::Init, _root: &Self::Root, sender: &ComponentSender) -> Self { 43 | let mut window = Child::::init((), &()); 44 | let canvas = Child::::init((), &window); 45 | 46 | window.set_text("Basic example"); 47 | window.set_size(Size::new(800.0, 600.0)); 48 | 49 | let sender = sender.clone(); 50 | spawn(async move { 51 | let mut interval = interval(Duration::from_secs(1)); 52 | loop { 53 | interval.tick().await; 54 | if !sender.post(MainMessage::Tick) { 55 | break; 56 | } 57 | } 58 | }) 59 | .detach(); 60 | Self { 61 | window, 62 | canvas, 63 | counter, 64 | } 65 | } 66 | 67 | async fn start(&mut self, sender: &ComponentSender) { 68 | let fut_window = self.window.start(sender, |e| match e { 69 | WindowEvent::Close => Some(MainMessage::Close), 70 | WindowEvent::Resize => Some(MainMessage::Redraw), 71 | _ => None, 72 | }); 73 | let fut_canvas = self.canvas.start(sender, |e| match e { 74 | CanvasEvent::Redraw => Some(MainMessage::Redraw), 75 | CanvasEvent::MouseDown(b) | CanvasEvent::MouseUp(b) => Some(MainMessage::Mouse(b)), 76 | CanvasEvent::MouseMove(p) => Some(MainMessage::MouseMove(p)), 77 | _ => None, 78 | }); 79 | futures_util::future::join(fut_window, fut_canvas).await; 80 | } 81 | 82 | async fn update(&mut self, message: Self::Message, sender: &ComponentSender) -> bool { 83 | futures_util::future::join(self.window.update(), self.canvas.update()).await; 84 | match message { 85 | MainMessage::Tick => { 86 | self.counter += 1; 87 | true 88 | } 89 | MainMessage::Close => { 90 | match MessageBox::new() 91 | .title("Basic example") 92 | .message("Close window?") 93 | .instruction("The window is about to close.") 94 | .style(MessageBoxStyle::Info) 95 | .buttons(MessageBoxButton::Yes | MessageBoxButton::No) 96 | .custom_button(CustomButton::new(114, "114")) 97 | .show(Some(&*self.window)) 98 | .await 99 | { 100 | MessageBoxResponse::Yes | MessageBoxResponse::Custom(114) => { 101 | sender.output(()); 102 | } 103 | _ => {} 104 | } 105 | false 106 | } 107 | MainMessage::Redraw => true, 108 | MainMessage::Mouse(_b) => { 109 | info!("{:?}", _b); 110 | false 111 | } 112 | MainMessage::MouseMove(_p) => { 113 | info!("{:?}", _p); 114 | false 115 | } 116 | } 117 | } 118 | 119 | fn render(&mut self, _sender: &ComponentSender) { 120 | self.window.render(); 121 | self.canvas.render(); 122 | 123 | let csize = self.window.client_size(); 124 | { 125 | let mut grid = Grid::from_str("1*,2*,1*", "1*,2*,1*").unwrap(); 126 | grid.push(&mut self.canvas).column(1).row(1).finish(); 127 | grid.set_size(csize); 128 | } 129 | 130 | let size = self.canvas.size(); 131 | let is_dark = ColorTheme::current() == ColorTheme::Dark; 132 | let brush = SolidColorBrush::new(if is_dark { 133 | Color::new(255, 255, 255, 255) 134 | } else { 135 | Color::new(0, 0, 0, 255) 136 | }); 137 | let mut ctx = self.canvas.context(); 138 | ctx.draw_ellipse( 139 | BrushPen::new(brush.clone(), 1.0), 140 | Rect::new((size.to_vector() / 4.0).to_point(), size / 2.0), 141 | ); 142 | ctx.draw_str( 143 | &brush, 144 | DrawingFontBuilder::new() 145 | .halign(HAlign::Center) 146 | .valign(VAlign::Center) 147 | .family("Arial") 148 | .size(12.0) 149 | .build(), 150 | (size.to_vector() / 2.0).to_point(), 151 | format!("Hello world!\nRunning: {}s", self.counter), 152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /examples/net.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use compio::{runtime::spawn, time::timeout}; 4 | use cyper::Client; 5 | use winio::{ 6 | App, Button, ButtonEvent, Canvas, CanvasEvent, Child, Color, ColorTheme, Component, 7 | ComponentSender, DrawingFontBuilder, Edit, HAlign, Layoutable, Orient, Point, Size, 8 | SolidColorBrush, StackPanel, VAlign, Window, WindowEvent, 9 | }; 10 | 11 | fn main() { 12 | #[cfg(feature = "enable_log")] 13 | tracing_subscriber::fmt() 14 | .with_max_level(compio_log::Level::INFO) 15 | .init(); 16 | 17 | App::new().run::("https://www.example.com/", &()); 18 | } 19 | 20 | struct MainModel { 21 | window: Child, 22 | canvas: Child, 23 | button: Child