├── .gitignore ├── Cargo.toml ├── README.md ├── contrib └── build-app.sh ├── examples ├── basic.rs ├── vst-webkit │ ├── Cargo.toml │ ├── contrib │ │ ├── vst-mac-bundle.sh │ │ └── vst-mac-install.sh │ └── src │ │ ├── dial.html │ │ ├── index.html │ │ ├── knob.html │ │ ├── lib.rs │ │ ├── lib2.rs │ │ ├── standalone.rs │ │ └── ui.rs ├── webkit.rs └── window.rs ├── screenshot.jpg ├── src ├── error.rs ├── events.rs ├── lib.rs └── platform │ ├── cocoa │ ├── app.rs │ ├── button.rs │ ├── color.rs │ ├── label.rs │ ├── mod.rs │ ├── rect.rs │ ├── responder.rs │ ├── slider.rs │ ├── utils.rs │ ├── webview.rs │ └── window.rs │ └── mod.rs └── tinyui-logo.png /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | .idea 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tinyui" 3 | version = "0.1.0" 4 | authors = ["Rob Saunders "] 5 | 6 | [lib] 7 | name="tinyui" 8 | crate-type = ["rlib"] 9 | 10 | [dependencies] 11 | objc = "0.2.2" 12 | cocoa = "0.14.0" 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-staticgen 2 | 3 | NOTE: This code is not ready for use yet. It can create basic things but the API changes frequently. 4 | 5 | A tiny native windowing and GUI library for rust. Currently supports MacOS with other platforms in the future (any help on this would be appreciated!). 6 | 7 | screenshot 8 | 9 | Major goals are: 10 | - **Simple Rust** - code should not use complicated rust features unless absolutely necessary. Should be usable by newcomers to rust to create simple and expressive guis. Apps are simple rust structs, events are simple enums. 11 | - **Native** - event driven, using native UI controls. 12 | - **Small Code*** - no complicated examples with pages of abstracted text. The example code is what you really get. 13 | - **Webkit** - optionally, webkit support is deep within the framework, so you can create an electron-like gui trivially (but not as garbage). 14 | 15 | It does NOT use a render loop like winit and almost all other gui frameworks with rust do, but works closer with the native operating systems render loop and uses callbacks, so it is far better on cpu and lends itself more to long-running applications you don't want sucking your cpu in the background. It also feels a billion (quantified, honest) times nicer to work with than something like winit, which is turning into a slushy garbage dump as it progresses :D 16 | 17 | Control names are stringly typed; this isn't likely to change in the future as to use a concrete type in this situtation is one of the (many) corner cases of rust, where it just doesn't work, and makes your code worse. Often UI developers will use complicated macros to get around simple problems, but here, stringly typed control ids during events are the simplest and least problematic method of use, and by far the easiest to understand. So this isn't likely to change in the future. 18 | 19 | Lots of inspiration taken from https://deterministic.space/elegant-apis-in-rust.html#what-makes-an-api-elegant 20 | 21 | Supports common controls like buttons and labels, and webkit (soon opengl) controls, natively, with absolute minimal dependencies. The library usage is loosely based on ggez. 22 | 23 | ## Usage 24 | 25 | To compile a basic example, run: 26 | ```bash 27 | cargo run --example window 28 | ``` 29 | 30 | To build and install the VST example: 31 | ```bash 32 | cd examples/vst-webkit 33 | bash contrib/vst-mac-bundle.sh 34 | ``` 35 | 36 | To run the VST example's GUI in standalone mode: 37 | ```bash 38 | cd examples/vst-webkit 39 | cargo run --bin standalone 40 | ``` 41 | 42 | -------------------------------------------------------------------------------- /contrib/build-app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Mac OSX .app builder 4 | 5 | function die { 6 | echo "ERROR: $1" > /dev/null 1>&2 7 | exit 1 8 | } 9 | 10 | if [ "$#" -ne 2 ]; then 11 | die "Usage: `basename $0` AppNameHere icon-file.svg" 12 | fi 13 | 14 | APPNAME=$1 15 | ICONNAME=$2 16 | 17 | if [ ! -f $ICONNAME ]; then 18 | die "Image file for icon not found" 19 | fi 20 | 21 | mkdir -p "$APPNAME.app/Contents/"{MacOS,Resources} 22 | 23 | cat > "$APPNAME.app/Contents/Info.plist" < 25 | 26 | 27 | 28 | CFBundleGetInfoString 29 | $APPNAME 30 | CFBundleExecutable 31 | $APPNAME 32 | CFBundleIdentifier 33 | com.example.www 34 | CFBundleName 35 | $APPNAME 36 | CFBundleIconFile 37 | icon.icns 38 | CFBundleShortVersionString 39 | 0.01 40 | CFBundleInfoDictionaryVersion 41 | 6.0 42 | CFBundlePackageType 43 | APPL 44 | IFMajorVersion 45 | 0 46 | IFMinorVersion 47 | 1 48 | NSHighResolutionCapable 49 | NSSupportsAutomaticGraphicsSwitching 50 | 51 | 52 | END 53 | 54 | cp $ICONNAME "$APPNAME.app/Contents/Resources/" 55 | cd "$APPNAME.app/Contents/Resources/" 56 | 57 | fileName=$ICONNAME 58 | postfix=${fileName##*.} 59 | 60 | if [[ $postfix == 'svg' ]]; then 61 | qlmanage -z -t -s 1024 -o ./ "$fileName" 62 | fileName=${fileName}.png 63 | fi 64 | 65 | echo $fileName 66 | 67 | mkdir icon.iconset 68 | 69 | sips -z 16 16 "$fileName" --out icon.iconset/icon_16x16.png 70 | sips -z 32 32 "$fileName" --out icon.iconset/icon_16x16@2x.png 71 | cp icon.iconset/icon_16x16@2x.png icon.iconset/icon_32x32.png 72 | sips -z 64 64 "$fileName" --out icon.iconset/icon_32x32@2x.png 73 | sips -z 128 128 "$fileName" --out icon.iconset/icon_128x128.png 74 | sips -z 256 256 "$fileName" --out icon.iconset/icon_128x128@2x.png 75 | cp icon.iconset/icon_128x128@2x.png icon.iconset/icon_256x256.png 76 | sips -z 512 512 "$fileName" --out icon.iconset/icon_256x256@2x.png 77 | cp icon.iconset/icon_256x256@2x.png icon.iconset/icon_512x512.png 78 | sips -z 1024 1024 "$fileName" --out icon.iconset/icon_512x512@2x.png 79 | 80 | # Create .icns file 81 | iconutil -c icns icon.iconset 82 | 83 | # Cleanup 84 | rm -R icon.iconset 85 | rm $fileName 86 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | extern crate tinyui; 2 | use tinyui::Window; 3 | use tinyui::{ App, Size, EventHandler, Event, WindowBuilder, WindowStyle }; 4 | 5 | const WIDTH: f64 = 480.; 6 | const HEIGHT: f64 = 320.; 7 | 8 | #[derive(Clone, Copy)] 9 | struct MyWindow { 10 | window: Window, 11 | } 12 | 13 | impl EventHandler for MyWindow { 14 | fn handle(&mut self, event: Event) { 15 | println!("-- event: {:?}", event); 16 | match event { 17 | Event::WindowWillClose => App::quit(), 18 | _ => (), 19 | } 20 | } 21 | } 22 | 23 | fn main() { 24 | let _ = match start_app() { 25 | Ok(_) => { App::run() }, 26 | Err(e) => println!("error: {:?}", e), 27 | }; 28 | } 29 | 30 | fn start_app() -> Result { 31 | let app = MyWindow { 32 | window: WindowBuilder { 33 | title: "Window Controls Example", 34 | style: WindowStyle::Default, 35 | size: Size { width: WIDTH, height: HEIGHT }, 36 | }.build()?, 37 | }; 38 | 39 | app.window.set_handler(app); 40 | Ok(app) 41 | } 42 | -------------------------------------------------------------------------------- /examples/vst-webkit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dd_overdrive_gui" 3 | version = "0.1.0" 4 | authors = ["Rob Saunders "] 5 | 6 | [dependencies] 7 | vst = { git = "https://github.com/rust-dsp/rust-vst" } 8 | tinyui = { path = "../../" } 9 | 10 | [lib] 11 | name = "dd_overdrive_gui" 12 | crate-type = ["cdylib"] 13 | 14 | # [profile.release] 15 | # lto = true 16 | 17 | [[bin]] 18 | name = "standalone" 19 | path = "./src/standalone.rs" 20 | -------------------------------------------------------------------------------- /examples/vst-webkit/contrib/vst-mac-bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make sure we have the arguments we need 4 | if [[ -z $1 || -z $2 ]]; then 5 | echo "Generates a macOS bundle from a compiled dylib file" 6 | echo "Example:" 7 | echo -e "\t$0 Plugin target/release/plugin.dylib" 8 | echo -e "\tCreates a Plugin.vst bundle" 9 | else 10 | # Make the bundle folder 11 | mkdir -p "$1.vst/Contents/MacOS" 12 | 13 | # Create the PkgInfo 14 | echo "BNDL????" > "$1.vst/Contents/PkgInfo" 15 | 16 | #build the Info.Plist 17 | echo " 18 | 19 | 20 | 21 | CFBundleDevelopmentRegion 22 | English 23 | 24 | CFBundleExecutable 25 | $1 26 | 27 | CFBundleGetInfoString 28 | vst 29 | 30 | CFBundleIconFile 31 | 32 | 33 | CFBundleIdentifier 34 | com.rust-vst2.$1 35 | 36 | CFBundleInfoDictionaryVersion 37 | 6.0 38 | 39 | CFBundleName 40 | $1 41 | 42 | CFBundlePackageType 43 | BNDL 44 | 45 | CFBundleVersion 46 | 1.0 47 | 48 | CFBundleSignature 49 | $((RANDOM % 9999)) 50 | 51 | CSResourcesFileMapped 52 | 53 | 54 | 55 | " > "$1.vst/Contents/Info.plist" 56 | 57 | # move the provided library to the correct location 58 | cp "$2" "$1.vst/Contents/MacOS/$1" 59 | 60 | echo "Created bundle $1.vst" 61 | fi -------------------------------------------------------------------------------- /examples/vst-webkit/contrib/vst-mac-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "./contrib/vst-mac-bundle.sh" ]; then 4 | echo "please run this script from the dd-plugs directory by typing ./contrib/vst-mac-install.sh" 5 | exit 1 6 | fi 7 | 8 | cargo build --all 9 | 10 | INSTALL_DIR="$HOME/Library/Audio/Plug-Ins/VST/" 11 | plugins=$(find target/debug/*.dylib -type f -exec basename {} \;) 12 | 13 | for plugin in $plugins; do 14 | DYLIB_FILE="target/debug/$plugin" 15 | # strip .dylib suffix 16 | TMP_VST_NAME=${plugin%.dylib} 17 | # replace _ with - 18 | TMP_VST_NAME_2=${TMP_VST_NAME//_/-} 19 | # strip lib prefix 20 | VST_NAME=${TMP_VST_NAME_2#lib} 21 | 22 | TARGET="$INSTALL_DIR$VST_NAME.vst" 23 | 24 | # remove the file if it exists in the target directory. 25 | [ -d "$TARGET" ] && rm -rf "$TARGET" 26 | 27 | bash ./contrib/vst-mac-bundle.sh $VST_NAME $DYLIB_FILE && 28 | mv -v ./$VST_NAME.vst $INSTALL_DIR 29 | done 30 | -------------------------------------------------------------------------------- /examples/vst-webkit/src/dial.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 187 | 188 | 211 | 212 | 213 | 214 |

DD-OVERDRIVEGUI

215 | 216 | 217 |
218 |
219 |
220 |
221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /examples/vst-webkit/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

DD-OVERDRIVEGUI

6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/vst-webkit/src/knob.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 360 | 361 | 367 | 368 | 435 | 436 |
437 | 438 |
439 | 440 | Min 441 | Max 442 | 443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 | 474 |
475 | 476 | 477 | -------------------------------------------------------------------------------- /examples/vst-webkit/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] extern crate vst; 2 | 3 | use vst::buffer::AudioBuffer; 4 | use vst::plugin::{Category, Plugin, Info }; 5 | use vst::editor::Editor; 6 | 7 | extern crate tinyui; 8 | use tinyui::*; 9 | 10 | mod ui; 11 | 12 | const WIDTH: f64 = 480.; 13 | const HEIGHT: f64 = 320.; 14 | 15 | struct DigiDist { 16 | window: Option, 17 | } 18 | 19 | impl Default for DigiDist { 20 | fn default() -> DigiDist { 21 | DigiDist { 22 | window: None, 23 | } 24 | } 25 | } 26 | impl Editor for DigiDist { 27 | fn size(&self) -> (i32, i32) { (WIDTH as i32, HEIGHT as i32) } 28 | fn position(&self) -> (i32, i32) { (0, 0) } 29 | fn is_open(&mut self) -> bool { true } 30 | fn close(&mut self) { self.window = None } 31 | 32 | fn open(&mut self, window: *mut std::os::raw::c_void) { 33 | self.window = Some(ui::PluginWindow::new(Window::new_with_parent(window).unwrap())); 34 | } 35 | } 36 | 37 | impl Plugin for DigiDist { 38 | fn get_info(&self) -> Info { 39 | Info { 40 | name: "DigiDistGUI".to_string(), 41 | vendor: "DeathDisco".to_string(), 42 | unique_id: 666997, 43 | category: Category::Effect, 44 | 45 | inputs: 2, 46 | outputs: 2, 47 | parameters: 0, 48 | 49 | // preset_chunks: true, 50 | 51 | ..Info::default() 52 | } 53 | } 54 | 55 | fn get_editor(&mut self) -> Option<&mut Editor> { Some(self) } 56 | } 57 | 58 | plugin_main!(DigiDist); -------------------------------------------------------------------------------- /examples/vst-webkit/src/lib2.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[macro_use] extern crate vst; 4 | 5 | use vst::buffer::AudioBuffer; 6 | use vst::plugin::{Category, Plugin, Info }; 7 | use vst::editor::Editor; 8 | 9 | extern crate tinyui; 10 | use tinyui::*; 11 | 12 | const WIDTH: f64 = 480.; 13 | const HEIGHT: f64 = 320.; 14 | 15 | struct DigiDist { 16 | threshold: f32, 17 | gain: f32, 18 | app: Option, 19 | ui: WebView, 20 | } 21 | 22 | struct PluginWindow { 23 | window: Window, 24 | label: Label, 25 | button: Button, 26 | } 27 | 28 | impl EventHandler for DigiDist { 29 | fn handle(&mut self, event: Event) { 30 | // self.label.set_text(&self.title); 31 | } 32 | } 33 | 34 | impl Default for DigiDist { 35 | fn default() -> DigiDist { 36 | let mut ui = WebView::new(Rect::new(0., 0., WIDTH, HEIGHT)); 37 | ui.load_html_string(include_str!("knob.html")); 38 | 39 | DigiDist { 40 | threshold: 1.0, // VST parameters are always 0.0 to 1.0 41 | gain: 1.0, 42 | app: None, 43 | ui: ui, 44 | } 45 | } 46 | } 47 | 48 | // impl Editor for DigiDist { 49 | // fn size(&self) -> (i32, i32) { (WIDTH as i32, HEIGHT as i32) } 50 | // fn position(&self) -> (i32, i32) { (0, 0) } 51 | // fn is_open(&mut self) -> bool { self.app.is_some() } 52 | // fn close(&mut self) { self.app = None } 53 | 54 | // fn open(&mut self, window: *mut std::os::raw::c_void) { 55 | // // let mut window = Window::new_with_parent(window).unwrap(); 56 | 57 | // // window.on_load(&on_load); 58 | // // window.set_title("oh hai!"); 59 | // // window.set_background_color(Color::green()); 60 | 61 | // // let mut label = Label::new("hello", Rect::new(10., 10., 300., 20.)); 62 | // // label.attach(&mut window); 63 | 64 | // // let mut button = ButtonBuilder { 65 | // // id: "a button", 66 | // // text: "click me", 67 | // // style: ButtonStyle::Square, 68 | // // position: Rect::new(10., 10., 300., 20.) }.build(); 69 | 70 | // // button.attach(&mut window); 71 | 72 | // // let mut webview = WebView::new(Rect::new(10., 10., 600., 400.)); 73 | // // self.ui.load_html_string("

HAHAFUCK U

"); 74 | 75 | // // button.on_click(Box::new(move || { 76 | // // label.set_text("hi"); 77 | // // })); 78 | 79 | // // let on_file_drop = std::cell::RefCell::new(Box::new(move |path:String| { 80 | // // println!("file got dropped bro: {:?}", path); 81 | // // button.set_text("dropper"); 82 | // // label.set_text(&path); 83 | // // })); 84 | 85 | // // self.ui.attach(&mut window); 86 | 87 | // // button.on_click(Some(Box::new( 88 | // // move |button| { 89 | // // label.set_text("hi"); 90 | // // button.set_text("hi"); 91 | // // } 92 | // // ))); 93 | 94 | // // window.on_file_drop(on_file_drop); 95 | 96 | // // window.run(); 97 | // // window.setup(); 98 | // // self.app = Some(window); 99 | // } 100 | 101 | // } 102 | 103 | impl Plugin for DigiDist { 104 | fn get_info(&self) -> Info { 105 | Info { 106 | name: "DigiDistGUI".to_string(), 107 | vendor: "DeathDisco".to_string(), 108 | unique_id: 666997, 109 | category: Category::Effect, 110 | 111 | inputs: 2, 112 | outputs: 2, 113 | parameters: 2, 114 | 115 | // preset_chunks: true, 116 | 117 | ..Info::default() 118 | } 119 | } 120 | 121 | // fn get_preset_data(&mut self) -> Vec { Vec::new() } 122 | // fn get_bank_data(&mut self) -> Vec { Vec::new() } 123 | // fn load_preset_data(&mut self, data: &[u8]) { } 124 | // fn load_bank_data(&mut self, data: &[u8]) { } 125 | 126 | fn get_parameter(&self, index: i32) -> f32 { 127 | match index { 128 | 0 => self.threshold, 129 | 1 => self.gain, 130 | _ => 0.0, 131 | } 132 | } 133 | 134 | fn set_parameter(&mut self, index: i32, value: f32) { 135 | match index { 136 | // We don't want to divide by zero, so we'll clamp the value 137 | 0 => self.threshold = value.max(0.01), 138 | 1 => self.gain = value, 139 | _ => (), 140 | } 141 | } 142 | 143 | fn get_parameter_name(&self, index: i32) -> String { 144 | match index { 145 | 0 => "Threshold".to_string(), 146 | 1 => "Gain".to_string(), 147 | _ => "".to_string(), 148 | } 149 | } 150 | 151 | fn get_parameter_text(&self, index: i32) -> String { 152 | match index { 153 | // Convert to a percentage 154 | 0 => format!("{}", self.threshold * 100.0), 155 | 1 => format!("{}", self.gain * 100.0), 156 | _ => "".to_string(), 157 | } 158 | } 159 | 160 | fn get_parameter_label(&self, index: i32) -> String { 161 | match index { 162 | 0 => "%".to_string(), 163 | 1 => "%".to_string(), 164 | _ => "".to_string(), 165 | } 166 | } 167 | 168 | fn process(&mut self, buffer: &mut AudioBuffer) { 169 | // Split out the input and output buffers into two vectors 170 | // let (input_buffer, mut output_buffer) = buffer.split(); 171 | 172 | // // Assume 2 channels 173 | // if input_buffer.len() < 2 || output_buffer.len() < 2 { 174 | // return; 175 | // } 176 | 177 | // // Iterate over inputs as (&f32, &f32) 178 | // let (l, r) = input_buffer.split_at(1); 179 | // let stereo_in = l[0].iter().zip(r[0].iter()); 180 | 181 | // // Iterate over outputs as (&mut f32, &mut f32) 182 | // let (mut l, mut r) = output_buffer.split_at_mut(1); 183 | // let stereo_out = l[0].iter_mut().zip(r[0].iter_mut()); 184 | 185 | // // Zip and process 186 | // for ((left_in, right_in), (left_out, right_out)) in stereo_in.zip(stereo_out) { 187 | 188 | // if *left_in >= 0.0 { 189 | // *left_out = left_in.min(self.threshold) / self.threshold * self.gain; 190 | // } 191 | // else { 192 | // *right_out = left_in.max(-self.threshold) / self.threshold * self.gain; 193 | // } 194 | 195 | // *left_out = *left_in + *left_out; 196 | // *right_out = *right_in + *right_out; 197 | // } 198 | } 199 | 200 | // fn get_editor(&mut self) -> Option<&mut Editor> { Some(self) } 201 | fn get_editor(&mut self) -> Option<&mut Editor> { None } 202 | } 203 | 204 | plugin_main!(DigiDist); 205 | -------------------------------------------------------------------------------- /examples/vst-webkit/src/standalone.rs: -------------------------------------------------------------------------------- 1 | extern crate tinyui; 2 | use tinyui::*; 3 | 4 | mod ui; 5 | 6 | const WIDTH: f64 = 480.; 7 | const HEIGHT: f64 = 320.; 8 | 9 | fn main() { 10 | let window = WindowBuilder { 11 | title: "Vst Plugin Example Standalone", 12 | style: WindowStyle::Default, 13 | size: Size { width: WIDTH, height: HEIGHT }, 14 | }.build().expect("window to build correctly"); 15 | 16 | let _ = ui::PluginWindow::new(window); 17 | let _ = App::run(); // start a cocoa runloop. not necessary on vsts. 18 | } 19 | -------------------------------------------------------------------------------- /examples/vst-webkit/src/ui.rs: -------------------------------------------------------------------------------- 1 | use tinyui::*; 2 | 3 | const WIDTH: f64 = 480.; 4 | const HEIGHT: f64 = 320.; 5 | 6 | #[derive(Clone, Copy)] 7 | pub struct PluginWindow { 8 | window: Window, 9 | button: Button, 10 | } 11 | 12 | impl EventHandler for PluginWindow { 13 | fn handle(&mut self, event: Event) { 14 | match event { 15 | // Event::WindowWillClose => App::quit(), // don't do this on a vst 16 | Event::WindowWillClose => {}, 17 | _ => (), 18 | } 19 | } 20 | } 21 | 22 | impl PluginWindow { 23 | pub fn new(mut window: Window) -> Self { 24 | let window_rect = Rect::new(0., 0., HEIGHT, WIDTH); 25 | let (_top_half_rect, bottom_half_rect) = window_rect.split_horizontal(); 26 | 27 | let mut app = Self { 28 | window: window, 29 | button: ButtonBuilder { 30 | id: "a button", 31 | text: "click me", 32 | style: ButtonStyle::Square, 33 | position: bottom_half_rect.inset(10.) }.build(), 34 | }; 35 | 36 | window.set_background_color(Color::red()); 37 | app.button.attach(&mut app.window); 38 | app.window.set_handler(app.clone()); 39 | 40 | app 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/webkit.rs: -------------------------------------------------------------------------------- 1 | extern crate tinyui; 2 | use tinyui::{ App, Color, WebView, EventHandler, Event, WindowBuilder, WindowStyle, Size }; 3 | 4 | const WIDTH: f64 = 480.; 5 | const HEIGHT: f64 = 320.; 6 | 7 | #[derive(Clone, Copy)] 8 | struct WebviewApp {} 9 | 10 | impl EventHandler for WebviewApp { 11 | fn handle(&mut self, event: Event) { 12 | println!("-- event: {:?}", event); 13 | match event { 14 | Event::WebEvent(t,n) => { 15 | match t.as_str() { 16 | "notification" => println!("yarrrr: {}", n), 17 | _ => (), 18 | } 19 | } 20 | Event::WindowWillClose => App::quit(), 21 | _ => (), 22 | } 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = WebviewApp{}; 28 | 29 | let mut window = WindowBuilder { 30 | title: "Webkit Example", 31 | style: WindowStyle::Default, 32 | size: Size { width: WIDTH, height: HEIGHT }, 33 | }.build().expect("Window did not create correctly."); 34 | 35 | window.set_background_color(Color::black()); 36 | 37 | let mut webview = WebView::new(window.frame()); 38 | webview.load_html_string(include_str!("vst-webkit/src/index.html")); 39 | webview.attach(&mut window); 40 | 41 | window.set_handler(app); 42 | window.run(); // not necessary on vsts. 43 | } 44 | -------------------------------------------------------------------------------- /examples/window.rs: -------------------------------------------------------------------------------- 1 | extern crate tinyui; 2 | use tinyui::Window; 3 | use tinyui::{ App, Size, Label, Rect, SliderBuilder, Slider, 4 | SliderStyle, ButtonBuilder, Button, ButtonStyle, EventHandler, Event, WindowBuilder, WindowStyle }; 5 | 6 | const WIDTH: f64 = 480.; 7 | const HEIGHT: f64 = 320.; 8 | 9 | #[allow(dead_code)] 10 | struct MyWindow { 11 | window: Window, 12 | label: Label, 13 | button: Button, 14 | button_on: Button, 15 | slider: Slider, 16 | slider_label: Label, 17 | } 18 | 19 | impl EventHandler for MyWindow { 20 | fn handle(&mut self, event: Event) { 21 | println!("-- event: {:?}", event); 22 | 23 | match event { 24 | Event::ButtonClicked(name) => { 25 | match name.as_str() { 26 | "a button" => { self.button.set_text("clicked me"); self.window.set_background_color(tinyui::Color::red()) }, 27 | "b button" => { self.button_on.set_text("clicked me too") ; self.window.set_background_color(tinyui::Color::green()) }, 28 | _ => () 29 | } 30 | }, 31 | Event::SliderUpdated(_, val) => self.slider_label.set_text(&format!("{:.2}", val)), 32 | Event::WindowWillClose => App::quit(), 33 | _ => () 34 | } 35 | } 36 | } 37 | 38 | fn main() { 39 | let window_rect = Rect::new(0., 0., HEIGHT, WIDTH); 40 | let (top_half_rect, bottom_half_rect) = window_rect.split_horizontal(); 41 | 42 | let mut app = MyWindow { 43 | label: Label::new("Cocoa Controls Demo", top_half_rect), 44 | slider: SliderBuilder { 45 | id: "a slider", 46 | value: 0.5, 47 | min_value: 0.0, 48 | max_value: 1.0, 49 | style: SliderStyle::Circular, 50 | position: top_half_rect}.build(), 51 | slider_label: Label::new("0.5", top_half_rect), 52 | button: ButtonBuilder { 53 | id: "a button", 54 | text: "click me", 55 | style: ButtonStyle::Square, 56 | position: bottom_half_rect.inset(10.) }.build(), 57 | button_on: ButtonBuilder { 58 | id: "b button", 59 | text: "click me", 60 | style: ButtonStyle::Square, 61 | position: top_half_rect.inset(10.) }.build(), 62 | window: WindowBuilder { 63 | title: "Window Controls Example", 64 | style: WindowStyle::Default, 65 | size: Size { width: WIDTH, height: HEIGHT }, 66 | }.build().expect("window did not create correctly"), 67 | }; 68 | 69 | // app.label.attach(&mut app.window); 70 | app.button.attach(&mut app.window); 71 | app.button_on.attach(&mut app.window); 72 | // app.slider.attach(&mut app.window); 73 | // app.slider_label.attach(&mut app.window); 74 | 75 | app.window.set_handler(app); 76 | 77 | let _ = App::run(); // not necessary on vsts. 78 | } 79 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monomadic/tinyui-rs/0beac1f626feb54d6b46fd4f6d2fb64072c790af/screenshot.jpg -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | #![allow(dead_code)] 3 | 4 | use std::error::Error; 5 | use std::fmt; 6 | 7 | #[derive(Debug, Clone)] 8 | pub enum TinyUIError { 9 | General(String), 10 | } 11 | 12 | impl fmt::Display for TinyUIError { 13 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 14 | fmt::Debug::fmt(&self, f) 15 | } 16 | } 17 | 18 | impl Error for TinyUIError { 19 | fn description(&self) -> &str { 20 | match *self { 21 | TinyUIError::General(ref e) => e, 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | #[derive(Clone, Debug)] 4 | pub enum Event { 5 | /// The window will close. 6 | WindowWillClose, 7 | 8 | /// A file has been dropped into the window. 9 | DroppedFile(PathBuf), 10 | 11 | /// A file has been dragged over the window and dragged out again. 12 | DraggingExited, 13 | 14 | /// A file has been dragged over the window. 15 | DraggingEntered(PathBuf), 16 | 17 | /// A native UI button was clicked. 18 | ButtonClicked(String), 19 | 20 | /// A notifier was sent from a WKWebView object. 21 | WebEvent(String, String), // change to String, String 22 | WebViewContentRecieved, 23 | WebViewStartedLoading, 24 | WebViewFinishedLoading, 25 | 26 | SliderUpdated(String, f32), 27 | } 28 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] extern crate objc; 2 | extern crate cocoa; 3 | 4 | mod platform; 5 | pub use platform::app::*; 6 | pub use platform::window::*; 7 | pub use platform::label::*; 8 | pub use platform::rect::*; 9 | pub use platform::color::*; 10 | pub use platform::button::*; 11 | pub use platform::slider::*; 12 | pub use platform::webview::*; 13 | 14 | mod events; 15 | pub use events::*; 16 | 17 | mod error; 18 | pub use error::*; 19 | -------------------------------------------------------------------------------- /src/platform/cocoa/app.rs: -------------------------------------------------------------------------------- 1 | use cocoa::appkit::{ NSApp, NSApplication }; 2 | use cocoa::base::{ nil }; 3 | 4 | pub struct App { 5 | } 6 | 7 | impl App { 8 | pub fn quit() { 9 | unsafe { 10 | let app = NSApp(); 11 | msg_send![app, terminate:nil]; 12 | } 13 | } 14 | 15 | pub fn run() { 16 | unsafe { 17 | let app = NSApp(); 18 | app.run(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/platform/cocoa/button.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | #![allow(unused_variables)] 3 | #![allow(unused_imports)] 4 | 5 | use cocoa::base::{ id, nil, NO }; 6 | use cocoa::appkit::{ NSButton }; 7 | use cocoa::foundation::{ NSString, NSAutoreleasePool }; 8 | use objc::runtime::{ Class, Object, Sel }; 9 | use objc::declare::ClassDecl; 10 | use Rect; 11 | use Window; 12 | use EventHandler; 13 | use Handler; 14 | use Event; 15 | use platform::platform::responder::send_event; 16 | use platform::platform::utils::*; 17 | 18 | use std::cell::RefCell; 19 | use std::os::raw::c_void; 20 | 21 | #[derive(Copy, Clone)] 22 | pub struct Button { 23 | id: id, 24 | } 25 | 26 | #[derive(Copy, Clone)] 27 | pub enum ButtonStyle { 28 | None = 0, 29 | Rounded = 1, 30 | Square = 2, 31 | Disclosure = 5, 32 | ShadowlessSquare = 6, 33 | Circular = 7, 34 | TexturedSquare = 8, 35 | Help = 9, 36 | SmallSquare = 10, 37 | TexturedRounded = 11, 38 | RoundedRect = 12, 39 | Recessed = 13, 40 | RoundedDisclosure = 14, 41 | Inline = 15, 42 | } 43 | 44 | pub struct ButtonBuilder { 45 | pub id: &'static str, 46 | pub text: &'static str, 47 | pub style: ButtonStyle, 48 | pub position: Rect, 49 | } 50 | 51 | impl ButtonBuilder { 52 | pub fn build(&self) -> Button { 53 | Button::new(self.id, self.text, self.style, self.position) 54 | } 55 | } 56 | 57 | use std; 58 | extern "C" fn onButtonClick(this: &Object, _cmd: Sel, target: id) { 59 | let name = unsafe { 60 | let ptr:u64 = *this.get_ivar("_name"); 61 | nsstring_decode(ptr as id) 62 | }; 63 | 64 | send_event(target, Event::ButtonClicked(name)); 65 | } 66 | 67 | impl Button { 68 | pub fn new(name: &str, text: &str, style: ButtonStyle, position: Rect) -> Self { 69 | 70 | // singleton class definition 71 | use std::sync::{Once, ONCE_INIT}; 72 | static mut RESPONDER_CLASS: *const Class = 0 as *const Class; 73 | static INIT: Once = ONCE_INIT; 74 | 75 | INIT.call_once(|| unsafe { 76 | let superclass = Class::get("NSObject").unwrap(); 77 | let mut decl = ClassDecl::new("ButtonResponder", superclass).unwrap(); 78 | 79 | decl.add_ivar::("_name"); 80 | 81 | decl.add_method(sel!(onButtonClick:), 82 | onButtonClick as extern fn(&Object, Sel, id)); 83 | 84 | RESPONDER_CLASS = decl.register(); 85 | }); 86 | 87 | let responder: id = unsafe { msg_send![RESPONDER_CLASS, new] }; 88 | let button = unsafe { 89 | let button = NSButton::alloc(nil).initWithFrame_(position.to_nsrect()); 90 | button.setTitle_(NSString::alloc(nil).init_str(text)); 91 | 92 | msg_send![button, setBezelStyle:style as u32]; 93 | 94 | msg_send![button, setTarget:responder]; 95 | msg_send![button, setAction:sel!(onButtonClick:)]; 96 | 97 | let objc_text = NSString::alloc(nil).init_str(name); 98 | (*responder).set_ivar("_name", objc_text as u64); 99 | 100 | Button { id: button } 101 | }; 102 | 103 | button 104 | } 105 | 106 | pub fn set_text(&mut self, text: &str) { 107 | unsafe { self.id.setTitle_(NSString::alloc(nil).init_str(text)) }; 108 | } 109 | 110 | pub fn attach(&mut self, window: &mut Window) { 111 | window.add_subview(self.id); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/platform/cocoa/color.rs: -------------------------------------------------------------------------------- 1 | use cocoa::base::{ id, class }; 2 | 3 | pub struct Color { 4 | id: id, 5 | } 6 | 7 | impl Color { 8 | pub fn red() -> Color { 9 | Color { id: unsafe{ msg_send![class("NSColor"), redColor] } } 10 | } 11 | 12 | pub fn green() -> Color { 13 | Color { id: unsafe{ msg_send![class("NSColor"), greenColor] } } 14 | } 15 | 16 | pub fn system_gray() -> Color { 17 | Color { id: unsafe{ msg_send![class("NSColor"), systemGrayColor] } } 18 | } 19 | 20 | pub fn black() -> Color { 21 | Color { id: unsafe{ msg_send![class("NSColor"), blackColor] } } 22 | } 23 | 24 | pub fn white() -> Color { 25 | Color { id: unsafe{ msg_send![class("NSColor"), whiteColor] } } 26 | } 27 | 28 | pub fn clear() -> Color { 29 | Color { id: unsafe{ msg_send![class("NSColor"), clearColor] } } 30 | } 31 | 32 | pub fn nscolor(&self) -> id { 33 | self.id 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/platform/cocoa/label.rs: -------------------------------------------------------------------------------- 1 | use cocoa::base::{ id, nil, NO }; 2 | use cocoa::appkit::NSTextField; 3 | use cocoa::foundation::{ NSString }; 4 | use Rect; 5 | use Window; 6 | 7 | #[derive(Copy, Clone)] 8 | pub struct Label { 9 | id: id, 10 | } 11 | 12 | // impl Drop for Label { 13 | // fn drop(&mut self) { 14 | // unsafe { msg_send![self.id, removeFromSuperview] }; 15 | // } 16 | // } 17 | 18 | impl Label { 19 | pub fn new(text: &str, position: Rect) -> Self { 20 | unsafe { 21 | let label = NSTextField::alloc(nil).initWithFrame_(position.to_nsrect()); 22 | label.setStringValue_(NSString::alloc(nil).init_str(text)); 23 | 24 | msg_send![label, setBezeled:NO]; 25 | msg_send![label, setDrawsBackground:NO]; 26 | msg_send![label, setEditable:NO]; 27 | msg_send![label, setSelectable:NO]; 28 | msg_send![label, setStringValue:NSString::alloc(nil).init_str(text)]; 29 | 30 | Label { id: label } 31 | } 32 | } 33 | 34 | pub fn set_text(&self, text: &str) { 35 | unsafe { self.id.setStringValue_(NSString::alloc(nil).init_str(text)) }; 36 | } 37 | 38 | pub fn attach(&mut self, window: &mut Window) { 39 | window.add_subview(self.id); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/platform/cocoa/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod window; 2 | pub mod label; 3 | pub mod rect; 4 | pub mod color; 5 | pub mod button; 6 | pub mod slider; 7 | pub mod webview; 8 | pub mod app; 9 | 10 | mod responder; 11 | 12 | pub mod utils; 13 | // use self::utils::*; 14 | -------------------------------------------------------------------------------- /src/platform/cocoa/rect.rs: -------------------------------------------------------------------------------- 1 | use cocoa::foundation::{ NSRect, NSPoint, NSSize }; 2 | 3 | #[derive(Copy, Clone, Debug)] 4 | pub struct Rect { 5 | pub origin: Point, 6 | pub size: Size, 7 | } 8 | 9 | #[derive(Copy, Clone, Debug)] 10 | pub struct Point { 11 | pub x: f64, 12 | pub y: f64, 13 | } 14 | 15 | #[derive(Copy, Clone, Debug)] 16 | pub struct Size { 17 | pub width: f64, 18 | pub height: f64, 19 | } 20 | 21 | impl Rect { 22 | pub fn new(x: f64, y: f64, w: f64, h:f64) -> Self { 23 | Rect{ origin: Point{ x: x, y: y }, size: Size{ width: w, height: h }} 24 | } 25 | 26 | pub fn from_nsrect(rect: NSRect) -> Self { 27 | Rect{ origin: Point{ x: rect.origin.y, y: rect.origin.x }, size: Size{ width: rect.size.height, height: rect.size.width }} 28 | } 29 | 30 | pub fn to_nsrect(&self) -> NSRect { 31 | NSRect::new(NSPoint::new(self.origin.y, self.origin.x), NSSize::new(self.size.height, self.size.width)) 32 | } 33 | 34 | pub fn split_vertical(self) -> (Rect, Rect) { 35 | ( 36 | Rect{ 37 | origin: Point{ x: self.origin.x, y: self.origin.y }, 38 | size: Size { width: self.size.width, height: self.size.height / 2. }}, 39 | Rect{ 40 | origin: Point { x: self.origin.x, y: self.size.height / 2. }, 41 | size: Size { width: self.size.width, height: self.size.height / 2. }} 42 | ) 43 | } 44 | 45 | pub fn split_horizontal(self) -> (Rect, Rect) { 46 | ( 47 | Rect{ 48 | origin: Point{ x: self.origin.x, y: self.origin.y }, 49 | size: Size { width: self.size.width / 2., height: self.size.height }}, 50 | Rect{ 51 | origin: Point { x: self.size.width / 2., y: self.origin.y }, 52 | size: Size { width: self.size.width / 2., height: self.size.height }} 53 | ) 54 | } 55 | 56 | pub fn inset(self, pts: f64) -> Rect { 57 | Rect { 58 | origin: Point{ x: self.origin.x + pts, y: self.origin.y + pts }, 59 | size: Size { width: self.size.width - (pts*2.), height: self.size.height - (pts*2.) }, 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/platform/cocoa/responder.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | #![allow(unused_variables)] 3 | 4 | use cocoa::base::{id}; 5 | 6 | use objc::runtime::{BOOL, YES}; 7 | use objc::runtime::{Class, Object, Sel}; 8 | use objc::declare::ClassDecl; 9 | 10 | use std::os::raw::c_void; 11 | use std::path::PathBuf; 12 | 13 | // use platform::platform::window::WindowEvents; 14 | 15 | extern "C" fn setViewController(this: &mut Object, _: Sel, controller: *mut c_void) { 16 | unsafe { 17 | this.set_ivar("ViewController", controller); 18 | } 19 | } 20 | 21 | pub fn set_event_handler_contained(responder: id, handler: Handler) { 22 | let boxed_handler = Box::new(handler); 23 | unsafe { 24 | let handler_ptr = Box::into_raw(boxed_handler) as *const Handler as *mut c_void; 25 | msg_send![responder, setEventHandler: handler_ptr]; 26 | } 27 | } 28 | 29 | pub extern "C" fn setEventHandler(this: &mut Object, _: Sel, handler: *mut c_void) { 30 | unsafe { this.set_ivar("EventHandler", handler) }; 31 | } 32 | 33 | pub fn send_event(target: id, event: Event) { 34 | let window: id = unsafe { msg_send![target, window] }; 35 | let responder: id = unsafe { msg_send![window, delegate] }; 36 | 37 | let handler_ptr: *mut c_void = unsafe { *(*responder).get_ivar("EventHandler") }; 38 | let mut handler: Box = unsafe { Box::from_raw(handler_ptr as *mut Handler) }; 39 | handler.handle(event); 40 | 41 | std::mem::forget(handler); // forget this memory so the id isn't deleted! 42 | } 43 | 44 | use { Handler, EventHandler, Event }; 45 | use std; 46 | 47 | /// Invoked when the image is released 48 | extern fn prepare_for_drag_operation(_: &Object, _: Sel, _: id) {} 49 | 50 | extern fn dragging_entered(this: &Object, _: Sel, sender: id) -> BOOL { 51 | let window: id = unsafe { msg_send![sender, draggingDestinationWindow] }; 52 | let mut handler = objc_retrieve_event_handler(window); 53 | 54 | let pb: id = unsafe { msg_send![sender, draggingPasteboard] }; 55 | let files = objc_get_files_from_pasteboard(pb); 56 | 57 | for file in files.into_iter() { 58 | handler.handle(Event::DraggingEntered(file)); 59 | } 60 | 61 | std::mem::forget(handler); 62 | 63 | YES 64 | } 65 | 66 | /// Invoked after the released image has been removed from the screen 67 | extern fn perform_drag_operation(this: &Object, _: Sel, sender: id) -> BOOL { 68 | let window: id = unsafe { msg_send![sender, draggingDestinationWindow] }; 69 | let mut handler = objc_retrieve_event_handler(window); 70 | 71 | let pb: id = unsafe { msg_send![sender, draggingPasteboard] }; 72 | 73 | let files = objc_get_files_from_pasteboard(pb); 74 | 75 | for file in files.into_iter() { 76 | handler.handle(Event::DroppedFile(file)); 77 | } 78 | 79 | std::mem::forget(handler); 80 | 81 | YES 82 | } 83 | 84 | fn objc_get_files_from_pasteboard(pasteboard: id) -> Vec { 85 | use cocoa::foundation::NSString; 86 | use cocoa::appkit::NSPasteboard; 87 | use cocoa::foundation::NSFastEnumeration; 88 | use cocoa::appkit; 89 | use std::ffi::CStr; 90 | 91 | let filenames = unsafe { NSPasteboard::propertyListForType(pasteboard, appkit::NSFilenamesPboardType) }; 92 | let mut files = Vec::new(); 93 | 94 | for file in unsafe { filenames.iter() } { 95 | let f = unsafe{ NSString::UTF8String(file) }; 96 | let path = unsafe { CStr::from_ptr(f).to_string_lossy().into_owned() }; 97 | let mut pathbuf = PathBuf::new(); 98 | pathbuf.push(path); 99 | files.push(pathbuf); 100 | }; 101 | 102 | files 103 | } 104 | 105 | fn objc_retrieve_event_handler(window: id) -> Box { 106 | let responder: id = unsafe { msg_send![window, delegate] }; 107 | let handler_ptr: *mut c_void = unsafe { *(*responder).get_ivar("EventHandler") }; 108 | let handler: Box = unsafe { Box::from_raw(handler_ptr as *mut Handler) }; 109 | handler 110 | } 111 | 112 | /// Invoked when the dragging operation is complete 113 | extern fn conclude_drag_operation(_: &Object, _: Sel, _: id) {} 114 | 115 | /// Invoked when the dragging operation is cancelled 116 | extern fn dragging_exited(this: &Object, _: Sel, sender: id) { 117 | let window: id = unsafe { msg_send![sender, draggingDestinationWindow] }; 118 | let mut handler = objc_retrieve_event_handler(window); 119 | handler.handle(Event::DraggingExited); 120 | std::mem::forget(handler); 121 | } 122 | 123 | extern fn window_closed(this: &Object, _: Sel, sender: id) { 124 | // let window: id = unsafe { msg_send![sender, delegate] }; 125 | // let mut handler = objc_retrieve_event_handler(sender); 126 | 127 | let window: id = unsafe { msg_send![sender, object] }; 128 | let responder: id = unsafe { msg_send![window, delegate] }; 129 | let handler_ptr: *mut c_void = unsafe { *(*responder).get_ivar("EventHandler") }; 130 | let mut handler: Box = unsafe { Box::from_raw(handler_ptr as *mut Handler) }; 131 | 132 | handler.handle(Event::WindowWillClose); 133 | std::mem::forget(handler); 134 | } 135 | 136 | // @property(readonly) BOOL acceptsFirstResponder; 137 | extern "C" fn acceptsFirstResponder(_: &Object, _: Sel) -> BOOL { 138 | // println!("acceptsFirstResponder() hit"); 139 | YES 140 | } 141 | 142 | // func acceptsFirstMouse(for event: NSEvent?) -> Bool 143 | extern "C" fn acceptsFirstMouse(_: &Object, _: Sel, theEvent: id) -> BOOL { 144 | // println!("acceptsFirstMouse() hit"); 145 | YES 146 | } 147 | 148 | extern "C" fn mouseEvent(this: &Object, _: Sel, mouseEvent: id) { 149 | use cocoa::appkit::NSEvent; 150 | // println!("NSEvent type: {:?}", unsafe { NSEvent::eventType(mouseEvent) }); 151 | } 152 | 153 | extern fn did_become_active(this: &Object, _: Sel, _: id) { 154 | // println!("focused"); 155 | } 156 | 157 | pub fn get_window_responder_class() -> *const Class { 158 | 159 | use std::sync::{Once, ONCE_INIT}; 160 | 161 | static mut RESPONDER_CLASS: *const Class = 0 as *const Class; 162 | static INIT: Once = ONCE_INIT; 163 | 164 | INIT.call_once(|| { 165 | let superclass = Class::get("NSView").unwrap(); 166 | let mut decl = ClassDecl::new("ViewResponder", superclass).unwrap(); 167 | 168 | decl.add_ivar::<*mut c_void>("ViewController"); 169 | decl.add_ivar::<*mut c_void>("EventHandler"); 170 | 171 | unsafe { 172 | 173 | decl.add_method(sel!(setViewController:), 174 | setViewController as 175 | extern "C" fn(this: &mut Object, _: Sel, _: *mut c_void)); 176 | 177 | decl.add_method(sel!(setEventHandler:), 178 | setEventHandler as 179 | extern "C" fn(this: &mut Object, _: Sel, _: *mut c_void)); 180 | 181 | decl.add_method(sel!(acceptsFirstResponder), 182 | acceptsFirstResponder as extern fn(this: &Object, _: Sel) -> BOOL); 183 | 184 | decl.add_method(sel!(acceptsFirstMouse:), 185 | acceptsFirstMouse as extern fn(this: &Object, _: Sel, _: id) -> BOOL); 186 | 187 | decl.add_method(sel!(applicationDidBecomeActive:), 188 | did_become_active as extern fn(&Object, Sel, id)); 189 | 190 | // func mouseDown(with event: NSEvent) 191 | // https://developer.apple.com/documentation/appkit/nsresponder/1524634-mousedown 192 | decl.add_method(sel!(mouseDown:), 193 | mouseEvent as extern fn(this: &Object, _: Sel, _: id)); 194 | 195 | decl.add_method(sel!(mouseUp:), 196 | mouseEvent as extern fn(this: &Object, _: Sel, _: id)); 197 | 198 | // callbacks for drag and drop events 199 | decl.add_method(sel!(draggingEntered:), 200 | dragging_entered as extern fn(&Object, Sel, id) -> BOOL); 201 | decl.add_method(sel!(prepareForDragOperation:), 202 | prepare_for_drag_operation as extern fn(&Object, Sel, id)); 203 | decl.add_method(sel!(performDragOperation:), 204 | perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL); 205 | decl.add_method(sel!(concludeDragOperation:), 206 | conclude_drag_operation as extern fn(&Object, Sel, id)); 207 | decl.add_method(sel!(draggingExited:), 208 | dragging_exited as extern fn(&Object, Sel, id)); 209 | 210 | decl.add_method(sel!(windowWillClose:), 211 | window_closed as extern fn(&Object, Sel, id)); 212 | 213 | RESPONDER_CLASS = decl.register(); 214 | } 215 | }); 216 | unsafe { RESPONDER_CLASS } 217 | } -------------------------------------------------------------------------------- /src/platform/cocoa/slider.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | #![allow(unused_variables)] 3 | #![allow(unused_imports)] 4 | 5 | use cocoa::base::{ id, nil, NO, class }; 6 | use cocoa::foundation::{ NSString, NSAutoreleasePool }; 7 | use objc::runtime::{ Class, Object, Sel }; 8 | use objc::declare::ClassDecl; 9 | use Rect; 10 | use Window; 11 | use EventHandler; 12 | use Handler; 13 | use Event; 14 | use platform::platform::responder::send_event; 15 | use platform::platform::utils::*; 16 | 17 | use std::cell::RefCell; 18 | use std::os::raw::c_void; 19 | 20 | #[derive(Copy, Clone)] 21 | pub struct Slider { 22 | id: id, 23 | } 24 | 25 | #[derive(Copy, Clone)] 26 | pub enum SliderStyle { 27 | 28 | // A bar-shaped slider automatically determines whether it’s horizontal or vertical by 29 | // the shape of its containing rectangle. If the slider is wider than it is tall, it’s 30 | // horizontal. Otherwise, it’s vertical. Use the initWithFrame: method to initialize a 31 | // slider, passing in an NSRect with the size and shape you want. 32 | Linear = 0, 33 | 34 | // For a circular slider, you must pass an NSRect at least large enough to contain the 35 | // control. For a regular circular slider, the NSRect must be at least 28 by 30 pixels. 36 | // For a small circular slider, it must be at least 18 by 20 pixels. Add 4 pixels in each 37 | // dimension if your slider has tick marks. 38 | Circular = 1, 39 | 40 | } 41 | 42 | pub struct SliderBuilder { 43 | pub id: &'static str, 44 | pub value: f64, 45 | pub min_value: f64, 46 | pub max_value: f64, 47 | pub style: SliderStyle, 48 | pub position: Rect, 49 | } 50 | 51 | impl SliderBuilder { 52 | pub fn build(&self) -> Slider { 53 | Slider::new(self.id, self.value, self.min_value, self.max_value, self.style, self.position) 54 | } 55 | } 56 | 57 | use std; 58 | extern "C" fn onSliderMove(this: &Object, _cmd: Sel, target: id) { 59 | let name = unsafe { 60 | let ptr:u64 = *this.get_ivar("_name"); 61 | nsstring_decode(ptr as id) 62 | }; 63 | 64 | let value:f64 = unsafe { msg_send![target, doubleValue] }; 65 | send_event(target, Event::SliderUpdated(name, value as f32)); 66 | } 67 | 68 | impl Slider { 69 | pub fn new(name: &str, value:f64, min:f64, max:f64, style: SliderStyle, position:Rect) -> Self { 70 | 71 | // singleton class definition 72 | use std::sync::{Once, ONCE_INIT}; 73 | static mut RESPONDER_CLASS: *const Class = 0 as *const Class; 74 | static INIT: Once = ONCE_INIT; 75 | 76 | INIT.call_once(|| unsafe { 77 | let superclass = Class::get("NSObject").expect("slider - NSObject to exist"); 78 | let mut decl = ClassDecl::new("SliderResponder", superclass).expect("slider - responder to declare"); 79 | 80 | decl.add_ivar::("_name"); 81 | 82 | decl.add_method(sel!(onMouseMove:), 83 | onSliderMove as extern fn(this: &Object, _: Sel, _: id)); 84 | 85 | RESPONDER_CLASS = decl.register(); 86 | }); 87 | 88 | let responder: id = unsafe { msg_send![RESPONDER_CLASS, new] }; 89 | let slider = unsafe { 90 | let slider:id = msg_send![class("NSSlider"), alloc]; 91 | let slider:id = msg_send![slider, initWithFrame:position.to_nsrect()]; 92 | 93 | msg_send![slider, setSliderType:style as u32]; 94 | 95 | msg_send![slider, setMinimumValue:min]; 96 | msg_send![slider, setMaximumValue:max]; 97 | msg_send![slider, setValue:value]; 98 | // msg_send![slider, setContinuous:true]; 99 | // msg_send![slider, setNumberOfTickMarks:10]; 100 | 101 | // use Color; 102 | // let layer:id = msg_send![slider, layer]; 103 | // msg_send![layer, setBackgroundColor:Color::red().nscolor()]; 104 | // msg_send![slider, setTrackFillColor:Color::red().nscolor()]; 105 | 106 | let objc_text = NSString::alloc(nil).init_str(name); 107 | (*responder).set_ivar("_name", objc_text as u64); 108 | 109 | msg_send![slider, setTarget:responder]; 110 | msg_send![slider, setAction:sel!(onMouseMove:)]; 111 | 112 | Slider { id: slider } 113 | }; 114 | 115 | slider 116 | } 117 | 118 | pub fn set_slider_type(&mut self, value: SliderStyle) { 119 | unsafe { msg_send![self.id, setSliderType:value as u32] }; 120 | } 121 | 122 | pub fn set_value(&mut self, value: f32) { 123 | unsafe { msg_send![self.id, setValue:value] }; 124 | } 125 | 126 | pub fn attach(&mut self, window: &mut Window) { 127 | window.add_subview(self.id); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/platform/cocoa/utils.rs: -------------------------------------------------------------------------------- 1 | use objc::runtime::{ Object }; 2 | use std; 3 | 4 | #[allow(dead_code)] 5 | pub fn print_nsstring(str: *mut Object) { 6 | use std::ffi::CStr; 7 | unsafe { 8 | let cstr: *const std::os::raw::c_char = msg_send![str, UTF8String]; 9 | let rstr = CStr::from_ptr(cstr).to_string_lossy().into_owned(); 10 | println!("{}", rstr); 11 | } 12 | } 13 | #[allow(dead_code)] 14 | pub fn nsstring_decode(str: *mut Object) -> String { 15 | use std::ffi::CStr; 16 | unsafe { 17 | let cstr: *const std::os::raw::c_char = msg_send![str, UTF8String]; 18 | let rstr = CStr::from_ptr(cstr).to_string_lossy().into_owned(); 19 | rstr 20 | } 21 | } -------------------------------------------------------------------------------- /src/platform/cocoa/webview.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | #![allow(unused_variables)] 3 | #![allow(unused_imports)] 4 | 5 | use cocoa; 6 | use cocoa::base::{ id, nil, NO, YES, class }; 7 | use cocoa::foundation::{ NSString }; 8 | use cocoa::appkit::{ NSColor }; 9 | 10 | use objc::runtime::{ Class, Object, Protocol, Sel }; 11 | use objc::declare::{ ClassDecl }; 12 | use objc; 13 | 14 | use std; 15 | 16 | use Rect; 17 | use Window; 18 | use Color; 19 | use { EventHandler, Event }; 20 | use platform::platform::responder::send_event; 21 | 22 | #[link(name = "WebKit", kind = "framework")] 23 | extern { 24 | pub static WKScriptMessageHandler: id; 25 | } 26 | 27 | #[derive(Copy, Clone)] 28 | pub struct WebView { 29 | id: id, 30 | } 31 | 32 | fn nsstring_to_str(string: id) -> String { 33 | let bytes = unsafe { 34 | let bytes: *const std::os::raw::c_char = msg_send![string, UTF8String]; 35 | bytes as *const u8 36 | }; 37 | let len = unsafe { string.len() }; 38 | unsafe { 39 | let bytes = std::slice::from_raw_parts(bytes, len); 40 | String::from_utf8(bytes.to_vec()).unwrap() 41 | } 42 | } 43 | 44 | pub fn wk_script_message_handler_class() -> &'static Class { 45 | use std::sync::{Once, ONCE_INIT}; 46 | 47 | static REGISTER_CUSTOM_SUBCLASS: Once = ONCE_INIT; 48 | REGISTER_CUSTOM_SUBCLASS.call_once(|| { 49 | let superclass = Class::get("WKUserContentController").unwrap(); 50 | let mut decl = ClassDecl::new("NotificationScriptMessageHandler", superclass).unwrap(); 51 | 52 | extern fn userContentController(this: &mut Object, _cmd: Sel, didReceive: bool, message: id) { 53 | let name = nsstring_to_str(unsafe { msg_send![message, name] }); 54 | let body = nsstring_to_str(unsafe { msg_send![message, body] }); 55 | 56 | let webview = unsafe { msg_send![message, webView] }; 57 | send_event(webview, Event::WebEvent(name, body)); 58 | } 59 | 60 | unsafe { 61 | decl.add_method(sel!(userContentController:didReceiveScriptMessage:), 62 | userContentController as extern fn(&mut Object, Sel, bool, id)); 63 | } 64 | 65 | decl.register(); 66 | }); 67 | 68 | Class::get("NotificationScriptMessageHandler").expect("NotificationScriptMessageHandler to be valid.") 69 | } 70 | 71 | pub fn navigation_delegate_class() -> &'static Class { 72 | use std::sync::{Once, ONCE_INIT}; 73 | 74 | static REGISTER_CUSTOM_SUBCLASS: Once = ONCE_INIT; 75 | REGISTER_CUSTOM_SUBCLASS.call_once(|| { 76 | let superclass = Class::get("WKWebView").expect("WKWebView to be available"); 77 | let mut decl = ClassDecl::new("NavigationDelegate", superclass).expect("WKWebView to be subclassable"); 78 | 79 | decl.add_protocol(Protocol::get("WKNavigationDelegate").expect("WKNavigationDelegate protocol to exist")); 80 | 81 | extern fn didCommitNavigation(this: &Object, _cmd: Sel, webview: id, navigation: id) { 82 | send_event(webview, Event::WebViewContentRecieved); 83 | } 84 | extern fn didFinishNavigation(this: &Object, _cmd: Sel, webview: id, navigation: id) { 85 | send_event(webview, Event::WebViewFinishedLoading); 86 | } 87 | 88 | unsafe { 89 | decl.add_method(sel!(webView:didCommitNavigation:), 90 | didCommitNavigation as extern fn(&Object, Sel, id, id)); 91 | decl.add_method(sel!(webView:didFinishNavigation:), 92 | didFinishNavigation as extern fn(&Object, Sel, id, id)); 93 | } 94 | 95 | decl.register(); 96 | }); 97 | 98 | Class::get("NavigationDelegate").expect("NavigationDelegate to be valid.") 99 | } 100 | 101 | impl WebView { 102 | pub fn new(position: Rect) -> Self { 103 | unsafe { 104 | 105 | // WKUserContentController 106 | let cls = wk_script_message_handler_class(); 107 | let scripthandler = { 108 | let obj: *mut Object = msg_send![cls, alloc]; 109 | let obj: *mut Object = msg_send![obj, init]; 110 | obj 111 | }; 112 | 113 | msg_send![scripthandler, addScriptMessageHandler:scripthandler name:NSString::alloc(nil).init_str("notification")]; 114 | 115 | // WKWebViewConfiguration 116 | let cls = Class::get("WKWebViewConfiguration").expect("WKWebViewConfiguration to exist"); 117 | let configuration = { 118 | let obj: *mut Object = msg_send![cls, alloc]; 119 | let obj: *mut Object = msg_send![obj, init]; 120 | obj 121 | }; 122 | 123 | // configuration.userContentController = scripthandler; 124 | msg_send![configuration, setUserContentController:scripthandler]; 125 | 126 | // WKWebView 127 | let cls = Class::get("WKWebView").expect("WKWebView to exist"); 128 | let webview = { 129 | let obj: *mut Object = msg_send![cls, alloc]; 130 | let obj: *mut Object = msg_send![obj, 131 | initWithFrame: position.to_nsrect() 132 | configuration: configuration ]; 133 | obj 134 | }; 135 | 136 | // WKNavigationDelegate 137 | let cls = navigation_delegate_class(); 138 | let navigation_delegate = { 139 | let obj: *mut Object = msg_send![cls, alloc]; 140 | let obj: *mut Object = msg_send![obj, init]; 141 | obj 142 | }; 143 | msg_send![webview, setNavigationDelegate:navigation_delegate]; 144 | 145 | // make window transparent 146 | msg_send![webview, setOpaque:NO]; 147 | msg_send![webview, setBackgroundColor:Color::clear().nscolor()]; 148 | 149 | WebView { 150 | id: webview, 151 | } 152 | } 153 | } 154 | 155 | pub fn set_size(&mut self, html: &str) { 156 | unsafe { msg_send![self.id, isLoading] } 157 | } 158 | 159 | pub fn is_loading(&mut self, html: &str) -> bool { 160 | unsafe { msg_send![self.id, isLoading] } 161 | } 162 | 163 | pub fn load_html_string(&mut self, html: &str) { 164 | unsafe { 165 | let cls = Class::get("NSURL").unwrap(); 166 | let nsurl = { 167 | let obj: *mut Object = msg_send![cls, fileURLWithPath:NSString::alloc(nil).init_str("")]; 168 | obj 169 | }; 170 | 171 | msg_send![self.id, 172 | loadHTMLString:NSString::alloc(nil).init_str(html) 173 | baseURL:nsurl]; 174 | 175 | msg_send![self.id, setOpaque:NO]; 176 | msg_send![self.id, setBackgroundColor:Color::clear().nscolor()]; 177 | } 178 | } 179 | 180 | pub fn attach(&mut self, window: &mut Window) { 181 | window.add_subview(self.id); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/platform/cocoa/window.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use cocoa::base::{ id, nil, NO, YES }; 4 | use cocoa::foundation::{ NSString, NSRect, NSSize, NSPoint }; 5 | use cocoa::appkit::{ NSApp, NSApplication, NSWindow, NSView, NSBackingStoreBuffered, NSRunningApplication, NSWindowStyleMask, 6 | NSApplicationActivateIgnoringOtherApps, NSApplicationActivationPolicyRegular, NSFilenamesPboardType }; 7 | 8 | use Color; 9 | use Rect; 10 | use error::*; 11 | 12 | #[derive(Copy, Clone)] 13 | pub struct Window { 14 | pub nswindow: id, 15 | pub nsview: id, 16 | } 17 | 18 | use Event; 19 | pub trait EventHandler { 20 | fn handle(&mut self, event: Event); 21 | } 22 | 23 | pub struct Handler { 24 | pub handler: Box, 25 | } 26 | 27 | impl EventHandler for Handler { 28 | fn handle(&mut self, event: Event) { 29 | self.handler.handle(event); 30 | } 31 | } 32 | 33 | // pub struct WindowEvents { 34 | // pub on_file_drop_callback: Option>>, 35 | // } 36 | 37 | // impl Drop for Window { 38 | // fn drop(&mut self) { 39 | // unsafe { self.nsview.removeFromSuperview() }; 40 | // } 41 | // } 42 | 43 | // impl WindowEvents { 44 | // pub fn on_mouse_down(&mut self) { 45 | // println!("Yaaaas!!"); 46 | // } 47 | 48 | // pub fn on_file_drop(&mut self, path: String) { 49 | // if let Some(ref mut callback) = self.on_file_drop_callback { 50 | // let ref mut file_drop = *(callback.borrow_mut()); 51 | // file_drop(path); 52 | // } 53 | // } 54 | // } 55 | 56 | pub enum WindowStyle { 57 | Default, 58 | } 59 | 60 | use Size; 61 | pub struct WindowBuilder { 62 | pub title: &'static str, 63 | pub style: WindowStyle, 64 | pub size: Size, 65 | } 66 | 67 | impl WindowBuilder { 68 | pub fn build(&self) -> Result { 69 | Ok(Window::new(self.title, self.size.width, self.size.height)?) 70 | } 71 | } 72 | 73 | impl Window { 74 | 75 | pub fn close(&mut self) { 76 | unsafe { 77 | self.nsview.removeFromSuperview(); 78 | }; 79 | 80 | unsafe { 81 | msg_send![self.nsview, release]; 82 | msg_send![self.nswindow, release]; 83 | }; 84 | 85 | 86 | self.nsview = nil; 87 | self.nswindow = nil; 88 | } 89 | 90 | pub fn set_handler(self, handler: H) { 91 | use platform::platform::responder::*; 92 | let responder: id = unsafe { msg_send![self.nswindow, delegate] }; 93 | set_event_handler_contained(responder, Handler{ handler: Box::new(handler) }); 94 | } 95 | 96 | pub fn new_with_parent(parent: *mut ::std::os::raw::c_void) -> Result { 97 | use cocoa::base::{id}; 98 | use objc::runtime::{Object}; 99 | 100 | Ok(Window { 101 | nswindow: unsafe{ msg_send![parent as *mut Object, window] }, 102 | nsview: parent as id, 103 | }) 104 | } 105 | 106 | /// Create a new Window from scratch. 107 | pub fn new(title: &str, width: f64, height: f64) -> Result { 108 | 109 | // set the responder class delegate 110 | use platform::platform::responder::*; 111 | let responder: id = unsafe { msg_send![get_window_responder_class(), new] }; 112 | 113 | let window = unsafe { NSWindow::alloc(nil) 114 | .initWithContentRect_styleMask_backing_defer_(NSRect::new(NSPoint::new(0., 0.), 115 | NSSize::new(width, height)), 116 | NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSClosableWindowMask, 117 | NSBackingStoreBuffered, 118 | NO) }; 119 | 120 | let view = unsafe { NSWindow::contentView(window) }; 121 | 122 | unsafe { 123 | msg_send![window, setDelegate:responder]; 124 | 125 | window.setAcceptsMouseMovedEvents_(YES); 126 | window.makeKeyAndOrderFront_(nil); 127 | window.makeFirstResponder_(view); 128 | 129 | let nstitle = NSString::alloc(nil).init_str(title); 130 | window.setTitle_(nstitle); 131 | 132 | let app = NSApp(); 133 | app.setActivationPolicy_(NSApplicationActivationPolicyRegular); 134 | app.activateIgnoringOtherApps_(YES); 135 | 136 | let current_app = NSRunningApplication::currentApplication(nil); 137 | current_app.activateWithOptions_(NSApplicationActivateIgnoringOtherApps); 138 | 139 | window.setOpaque_(YES); 140 | window.center(); 141 | 142 | use cocoa::foundation::NSArray; 143 | 144 | // register for drag and drop operations. 145 | msg_send![window, 146 | registerForDraggedTypes:NSArray::arrayWithObject(nil, NSFilenamesPboardType)]; 147 | } 148 | 149 | Ok(Window { 150 | nswindow: window, 151 | nsview: view, 152 | }) 153 | } 154 | 155 | pub fn make_resizable(&mut self) { 156 | unsafe { self.nswindow.setStyleMask_(self.nswindow.styleMask() | NSWindowStyleMask::NSResizableWindowMask) }; 157 | } 158 | 159 | pub fn set_title(&mut self, title: &str) { 160 | unsafe { 161 | let nstitle = NSString::alloc(nil).init_str(title); 162 | self.nswindow.setTitle_(nstitle); 163 | } 164 | } 165 | 166 | pub fn set_background_color(&mut self, color: Color) { 167 | unsafe { NSWindow::setBackgroundColor_(self.nswindow, color.nscolor()) }; 168 | } 169 | 170 | pub fn run(&mut self) { 171 | unsafe { 172 | let app = NSApp(); 173 | app.run(); 174 | } 175 | } 176 | 177 | pub fn add_subview(&mut self, view: id) { 178 | unsafe { NSView::addSubview_(self.nsview, view) }; 179 | } 180 | 181 | pub fn frame(&self) -> Rect { 182 | Rect::from_nsrect(unsafe { NSView::frame(self.nsview) }) 183 | } 184 | 185 | pub fn bounds(&self) -> Rect { 186 | Rect::from_nsrect(unsafe { NSView::bounds(self.nsview) }) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/platform/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | #[path="cocoa/mod.rs"] 3 | pub mod platform; 4 | 5 | #[cfg(target_os = "windows")] 6 | panic!("library not supported on windows"); 7 | 8 | pub use self::platform::*; 9 | -------------------------------------------------------------------------------- /tinyui-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monomadic/tinyui-rs/0beac1f626feb54d6b46fd4f6d2fb64072c790af/tinyui-logo.png --------------------------------------------------------------------------------