├── .gitignore ├── .gitmodules ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── LICENSE-MIT ├── LICENSE-ZLIB ├── README.md ├── examples ├── demo-clock.rs ├── demo-cutout.rs ├── demo-glutin.rs ├── demo-text.rs ├── demo-transform.rs └── demo-ui.rs ├── nanovg-sys ├── Cargo.toml ├── build.rs ├── glad │ ├── glad.c │ └── glad.h ├── lib.rs └── nanovg_shim.c ├── resources ├── Mechanic of the Heart.ttf ├── NotoEmoji-Regular.ttf ├── Roboto-Bold.ttf ├── Roboto-Regular.ttf ├── entypo.ttf ├── images │ ├── image1.jpg │ ├── image10.jpg │ ├── image11.jpg │ ├── image12.jpg │ ├── image2.jpg │ ├── image3.jpg │ ├── image4.jpg │ ├── image5.jpg │ ├── image6.jpg │ ├── image7.jpg │ ├── image8.jpg │ └── image9.jpg └── lenna.png ├── rustfmt.toml ├── screenshots ├── README.md ├── demo-clock.png ├── demo-cutout.gif ├── demo-glutin.png ├── demo-text.png ├── demo-transform.png └── demo-ui.png └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | lib/ 3 | tmp/ 4 | doc/ 5 | target/ 6 | rust-empty.mk 7 | watch.sh 8 | target/ 9 | *.swp 10 | *.orig 11 | Cargo.lock 12 | examples/demo/dump.png 13 | *.DS_Store 14 | .idea/ 15 | .vscode/ 16 | nanovg-rs.iml -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "nanovg"] 2 | path = nanovg-sys/nanovg 3 | url = https://github.com/memononen/nanovg 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - nightly 5 | matrix: 6 | allow_failures: 7 | - rust: nightly 8 | script: 9 | - cargo build --features "gl3" 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at kevin@kelleysoft.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nanovg" 3 | version = "1.0.2" 4 | authors = ["Kevin Kelley "] 5 | readme = "README.md" 6 | description = "Idiomatic bindings to the NanoVG library" 7 | homepage = "https://github.com/KevinKelley/nanovg-rs" 8 | repository = "https://github.com/KevinKelley/nanovg-rs" 9 | keywords = ["nanovg", "bindings", "vector", "graphics", "opengl"] 10 | categories = ["rendering::graphics-api", "games", "gui"] 11 | license = "MIT/Zlib" 12 | 13 | [package.metadata.docs.rs] 14 | features = ["gl3"] 15 | 16 | [lib] 17 | name = "nanovg" 18 | 19 | [features] 20 | gl2 = ["nanovg-sys/gl2"] 21 | gl3 = ["nanovg-sys/gl3"] 22 | gles2 = ["nanovg-sys/gles2"] 23 | gles3 = ["nanovg-sys/gles3"] 24 | default = ["gl3"] 25 | 26 | [dependencies] 27 | nanovg-sys = { path = "nanovg-sys", version = "1.0.2" } 28 | 29 | [dev-dependencies] 30 | glutin = "0.13.0" 31 | gl = "0.10.0" 32 | chrono = "0.4" 33 | rand = "0.4" 34 | lazy_static = "1.0" 35 | 36 | [[example]] 37 | name = "demo-glutin" 38 | 39 | [[example]] 40 | name = "demo-clock" 41 | 42 | [[example]] 43 | name = "demo-ui" 44 | 45 | [[example]] 46 | name = "demo-cutout" 47 | 48 | [[example]] 49 | name = "demo-text" 50 | 51 | [[example]] 52 | name = "demo-transform" 53 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Kevin Kelley 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. -------------------------------------------------------------------------------- /LICENSE-ZLIB: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Kevin Kelley 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NanoVG - Rust Wrapper 2 | 3 | NanoVG-RS is a wrapper around the [NanoVG vector graphics library](https://github.com/memononen/nanovg) for the Rust programming language. 4 | 5 | > NanoVG is small antialiased vector graphics rendering library for OpenGL. It has lean API modeled after HTML5 canvas API. It is aimed to be a practical and fun toolset for building scalable user interfaces and visualizations. 6 | 7 | NanoVG-RS provides a fully featured, functional, high-level and Rust-idiomatic API on top of the NanoVG C-API. 8 | 9 | # Building 10 | 11 | We recommend grabbing the latest release from [crates.io](https://crates.io/crates/nanovg). 12 | 13 | Alternatively, you can clone and build the library yourself: 14 | 15 | git clone --recursive https://github.com/KevinKelley/nanovg-rs 16 | cd nanovg-rs 17 | cargo build --features "gl3" 18 | 19 | This library comes with a couple examples: 20 | - very useful example called `demo-glutin`. If you want to make sure that nanovg is working on your system, clone and build this crate as shown above and run the command `cargo run --example demo-glutin --features="gl3"`. This should produce a window similar to that below. 21 | - a clock example, 'demo-clock', because who doesn't like clocks? And I needed to get rotation transforms working. Run it with `cargo run --example demo-clock --features "gl3"` 22 | 23 | **Note** that when running the examples, the needed resources might not be found if you run it without a `cargo run --example` command. Thist is just a working-directory path issue. 24 | 25 | Usage 26 | ===== 27 | 28 | Add the following to your `Cargo.toml`: 29 | 30 | ```toml 31 | [dependencies.nanovg] 32 | version = "Use the latest version from crates.io" 33 | features = ["glX"] 34 | ``` 35 | 36 | `glX` can be exactly one of `gl2`, `gl3`, `gles2` or `gles3`, 37 | to specify the version of OpenGL to use. Use `gl3` or `gl2` for computers and `gles3` or `gles2` for mobile devices. 38 | 39 | **TODO: SIMPLE API GUIDE** 40 | 41 | # Screenshots 42 | 43 | You can see more screenshots [here](/screenshots). 44 | 45 | ![demo-ui](/screenshots/demo-ui.png) 46 | Output of the `demo-ui` example. 47 | 48 | # Interesting Links 49 | 50 | - [Blendish](https://bitbucket.org/duangle/blendish) 51 | - [NanoVG](https://github.com/memononen/nanovg) 52 | - [gl-rs](https://github.com/bjz/gl-rs) 53 | - [Glutin](https://github.com/tomaka/glutin) 54 | - [glfw-rs](https://github.com/PistonDevelopers/glfw-rs) 55 | 56 | # License and Credits 57 | 58 | The binding is licensed under [the MIT license](LICENSE.txt). 59 | NanoVG is released under the zlib license. 60 | 61 | Test-font *Mechanic of the Heart* by [Agathe M.Joyce](https://www.dafont.com/agathe-m-joyce.d6546). -------------------------------------------------------------------------------- /examples/demo-clock.rs: -------------------------------------------------------------------------------- 1 | extern crate chrono; 2 | extern crate gl; 3 | extern crate glutin; 4 | extern crate nanovg; 5 | 6 | use chrono::prelude::*; 7 | use glutin::GlContext; 8 | use nanovg::{ 9 | Alignment, BasicCompositeOperation, Color, CompositeOperation, Font, Gradient, PathOptions, StrokeOptions, 10 | TextOptions, Transform, 11 | }; 12 | use std::f32::consts::PI; 13 | use std::{thread, time}; 14 | use std::env; 15 | 16 | const INIT_WINDOW_SIZE: (u32, u32) = (480, 480); 17 | 18 | //fn main() -> Result<(), Box> { 19 | fn main() { 20 | let mut events_loop = glutin::EventsLoop::new(); 21 | let window = glutin::WindowBuilder::new() 22 | .with_title("NanoVG Clock") 23 | .with_dimensions(INIT_WINDOW_SIZE.0, INIT_WINDOW_SIZE.1); 24 | let context = glutin::ContextBuilder::new() 25 | .with_vsync(true) 26 | .with_multisampling(4) 27 | .with_srgb(true); 28 | let gl_window = glutin::GlWindow::new(window, context, &events_loop).unwrap(); 29 | 30 | unsafe { 31 | gl_window.make_current().unwrap(); 32 | gl::load_with(|symbol| gl_window.get_proc_address(symbol) as *const _); 33 | gl::ClearColor(0.0, 0.0, 0.0, 1.0); 34 | } 35 | let context = nanovg::ContextBuilder::new() 36 | .stencil_strokes() 37 | .build() 38 | .expect("Initialization of NanoVG failed!"); 39 | 40 | // match env::current_exe() { 41 | // Ok(exe_path) => println!("Path of this executable is: {}", 42 | // exe_path.display()), 43 | // Err(e) => println!("failed to get current exe path: {e}"), 44 | // }; 45 | 46 | let exe_path = env::current_exe().expect("can't find current dir"); 47 | println!("Path of this executable is: {}", exe_path.display()); 48 | 49 | let path = exe_path.parent().expect("doesn't seem to have a parent?!"); 50 | println!("Parent path is: {}", path.display()); 51 | 52 | let mut filename = path.to_path_buf(); 53 | filename.push("resources"); 54 | println!("resourses path is: {}", filename.display()); 55 | 56 | filename.push("Roboto-Regular.ttf"); 57 | println!("font file is: {}", filename.display()); 58 | 59 | let filename = filename.to_str().expect("opps"); 60 | println!("{}", filename); 61 | 62 | let roboto_font = Font::from_file(&context, "Roboto", filename) 63 | .expect("Failed to load font 'Roboto-Regular.ttf'"); 64 | 65 | 66 | 67 | // let roboto_font = Font::from_file(&context, "Roboto", "resources/Roboto-Regular.ttf") 68 | // .expect("Failed to load font 'Roboto-Regular.ttf'"); 69 | 70 | let mut running = true; 71 | 72 | let mut prev_second = -1.0; 73 | 74 | while running { 75 | events_loop.poll_events(|event| match event { 76 | glutin::Event::WindowEvent { event, .. } => match event { 77 | glutin::WindowEvent::Closed => running = false, 78 | glutin::WindowEvent::Resized(w, h) => gl_window.resize(w, h), 79 | _ => {} 80 | }, 81 | _ => {} 82 | }); 83 | 84 | let dt: DateTime = Local::now(); // e.g. `2014-11-28T21:45:59.324310806+09:00` 85 | let hour = dt.hour(); 86 | let am = hour < 12; 87 | let hour: f32 = f64::from(hour % 12) as f32; 88 | let minute: f32 = f64::from(dt.minute()) as f32; 89 | let second: f32 = f64::from(dt.second()) as f32; 90 | let year: i32 = dt.year(); 91 | let month: u32 = dt.month(); 92 | let day: u32 = dt.day(); 93 | 94 | // don't bother re-draw unless time has changed 95 | if second == prev_second { 96 | let frame_time = time::Duration::from_millis(33); 97 | thread::sleep(frame_time); 98 | } else { 99 | prev_second = second; 100 | } 101 | 102 | let (width, height) = gl_window.get_inner_size().unwrap(); 103 | let (width, height) = (width as i32, height as i32); 104 | 105 | unsafe { 106 | gl::Viewport(0, 0, width, height); 107 | gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT | gl::STENCIL_BUFFER_BIT); 108 | } 109 | 110 | // round clock size is minimum of height and width 111 | let clock_size = width.min(height) - 2; 112 | 113 | let font_size = 24.0; 114 | 115 | let origin = (0.0, 0.0); // upper-left corner 116 | let dial_center = (f64::from(width) as f32 / 2.0, f64::from(height) as f32 / 2.0); 117 | let dial_radius: f32 = f64::from(clock_size / 2) as f32; 118 | let second_hand_len = dial_radius * 0.9; 119 | let minute_hand_len = dial_radius * 0.8; 120 | let hour_hand_len = dial_radius * 0.6; 121 | 122 | let two_pi = 2.0 * PI; 123 | let radians_per_sec = two_pi / 60.0; 124 | let radians_per_hour = two_pi / 12.0; 125 | 126 | let white: Color = Color::new(1.0, 1.0, 1.0, 1.0); 127 | let silver: Color = Color::from_rgb(196, 199, 206); 128 | let darksilver: Color = Color::from_rgb(148, 152, 161); 129 | let darkgray: Color = Color::from_rgb(169, 169, 169); 130 | let dial_color = Color::new(0.2, 0.0, 0.8, 1.0); 131 | 132 | context.frame((width as f32, height as f32), gl_window.hidpi_factor(), |frame| { 133 | // hour/minute markers 134 | 135 | //let sigils = ["XII", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"]; 136 | let sigils: Vec = (0..13).map(|n| format!("{}", n)).collect(); 137 | for h in 1..13 { 138 | let j = f64::from(h) as f32; 139 | let x = dial_center.0 + (second_hand_len * (j * radians_per_hour).sin()); 140 | let y = dial_center.1 - (second_hand_len * (j * radians_per_hour).cos()); 141 | frame.text( 142 | roboto_font, 143 | (x, y), 144 | &sigils[h as usize], 145 | TextOptions { 146 | color: silver, 147 | size: font_size, 148 | align: Alignment::new().center().middle(), 149 | ..Default::default() 150 | }, 151 | ); 152 | } 153 | 'ticks: for m in 1..61 { 154 | if m % 5 == 0 { 155 | continue 'ticks; 156 | } 157 | let m = f64::from(m) as f32; 158 | let ticks_radius = dial_radius * 0.925; 159 | let tick_len = 3.0; 160 | let tick_width = 1.0; 161 | frame.path( 162 | |path| { 163 | path.move_to((0.0, -ticks_radius)); 164 | path.line_to((0.0, -ticks_radius - tick_len)); 165 | path.close(); 166 | path.stroke( 167 | white, 168 | StrokeOptions { 169 | width: tick_width, 170 | ..Default::default() 171 | }, 172 | ); 173 | path.fill(white, Default::default()); 174 | }, 175 | PathOptions { 176 | composite_operation: CompositeOperation::Basic(BasicCompositeOperation::Lighter), 177 | alpha: 1.0, 178 | transform: Some( 179 | Transform::new() 180 | .translate(dial_center.0, dial_center.1) 181 | .rotate(m * radians_per_sec), 182 | ), 183 | ..Default::default() 184 | }, 185 | ); 186 | } 187 | 188 | // time-string 189 | let show_time_string = false; 190 | if show_time_string { 191 | frame.text( 192 | roboto_font, 193 | (dial_center.0, dial_center.1 + dial_radius * 0.7 - font_size), 194 | format!("{}:{:02}:{:02} {}", hour, minute, second, if am { "AM" } else { "PM" }), 195 | TextOptions { 196 | color: silver, 197 | size: font_size, 198 | align: Alignment::new().center().baseline(), 199 | ..Default::default() 200 | }, 201 | ); 202 | } 203 | // date-string 204 | frame.text( 205 | roboto_font, 206 | (dial_center.0, dial_center.1 + dial_radius * 0.7), 207 | format!("{:4}-{:02}-{:02}", year, month, day), 208 | TextOptions { 209 | color: silver, 210 | size: font_size, 211 | align: Alignment::new().center().baseline(), 212 | ..Default::default() 213 | }, 214 | ); 215 | 216 | //Draw the dial 217 | frame.path( 218 | |path| { 219 | path.circle(origin, dial_radius); 220 | path.stroke( 221 | silver, 222 | StrokeOptions { 223 | width: 3.0, 224 | ..Default::default() 225 | }, 226 | ); 227 | path.fill(dial_color, Default::default()); 228 | }, 229 | PathOptions { 230 | composite_operation: CompositeOperation::Basic(BasicCompositeOperation::Lighter), 231 | alpha: 1.0, //elapsed.cos() * 0.5 + 0.5, 232 | transform: Some(Transform::new().translate(dial_center.0, dial_center.1)), 233 | ..Default::default() 234 | }, 235 | ); 236 | 237 | let draw_hand = |theta: f32, length: f32, width: f32| { 238 | frame.path( 239 | |path| { 240 | path.move_to(origin); 241 | path.line_to((0.0, -length)); 242 | path.close(); 243 | path.stroke( 244 | white, 245 | StrokeOptions { 246 | width: width, 247 | ..Default::default() 248 | }, 249 | ); 250 | path.fill(white, Default::default()); 251 | }, 252 | PathOptions { 253 | composite_operation: CompositeOperation::Basic(BasicCompositeOperation::Lighter), 254 | alpha: 1.0, 255 | transform: Some(Transform::new().translate(dial_center.0, dial_center.1).rotate(theta)), 256 | ..Default::default() 257 | }, 258 | ); 259 | }; 260 | 261 | // draw the hands 262 | 263 | //let hour_angle = hour*radians_per_hour + minute*PI/360.0; 264 | let hour_angle = (((hour * 60.0 + minute) / 60.0) / 12.0) * two_pi; 265 | let minute_angle = minute * radians_per_sec; 266 | let second_angle = second * radians_per_sec; 267 | 268 | draw_hand(second_angle, second_hand_len, 1.0); 269 | draw_hand(minute_angle, minute_hand_len, 3.0); 270 | draw_hand(hour_angle, hour_hand_len, 5.0); 271 | 272 | //Draw the boss 273 | frame.path( 274 | |path| { 275 | let boss_rad = 6.0; 276 | path.circle(origin, boss_rad); 277 | path.stroke( 278 | darkgray, 279 | StrokeOptions { 280 | width: 1.0, 281 | ..Default::default() 282 | }, 283 | ); 284 | path.fill( 285 | Gradient::Radial { 286 | center: origin, 287 | inner_radius: 0.0, 288 | outer_radius: boss_rad, 289 | start_color: silver, 290 | end_color: darksilver, 291 | }, 292 | Default::default(), 293 | ); 294 | }, 295 | PathOptions { 296 | composite_operation: CompositeOperation::Basic(BasicCompositeOperation::SourceOver), 297 | alpha: 1.0, 298 | transform: Some(Transform::new().translate(dial_center.0, dial_center.1)), 299 | ..Default::default() 300 | }, 301 | ); 302 | }); 303 | 304 | gl_window.swap_buffers().unwrap(); 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /examples/demo-cutout.rs: -------------------------------------------------------------------------------- 1 | extern crate gl; 2 | extern crate glutin; 3 | extern crate nanovg; 4 | extern crate rand; 5 | 6 | #[macro_use] 7 | extern crate lazy_static; 8 | 9 | use glutin::GlContext; 10 | use nanovg::{Color, Frame, PathOptions, Solidity, StrokeOptions, Transform, Winding}; 11 | use rand::{thread_rng, Rand, Rng}; 12 | use std::collections::HashMap; 13 | use std::f32::consts::PI; 14 | use std::time::Instant; 15 | 16 | const INIT_WINDOW_SIZE: (u32, u32) = (1200, 825); 17 | 18 | lazy_static! { 19 | static ref COLORS: [Color; 4] = [ 20 | Color::from_rgb(0x00, 0xBF, 0xA8), 21 | Color::from_rgb(0x99, 0x66, 0xFF), 22 | Color::from_rgb(0xFF, 0x64, 0x64), 23 | Color::from_rgb(0x00, 0xC8, 0xFF) 24 | ]; 25 | } 26 | 27 | fn main() { 28 | let mut events_loop = glutin::EventsLoop::new(); 29 | let window = glutin::WindowBuilder::new() 30 | .with_title("NanoVG Cutout") 31 | .with_dimensions(INIT_WINDOW_SIZE.0, INIT_WINDOW_SIZE.1); 32 | let context = glutin::ContextBuilder::new() 33 | .with_vsync(false) 34 | .with_multisampling(4) 35 | .with_srgb(true); 36 | let gl_window = glutin::GlWindow::new(window, context, &events_loop).unwrap(); 37 | 38 | unsafe { 39 | gl_window.make_current().unwrap(); 40 | gl::load_with(|symbol| gl_window.get_proc_address(symbol) as *const _); 41 | } 42 | 43 | let context = nanovg::ContextBuilder::new() 44 | .stencil_strokes() 45 | .build() 46 | .expect("Initialization of NanoVG failed!"); 47 | 48 | let mut rng = thread_rng(); 49 | let mut running = true; 50 | let mut mouse = (0.0f32, 0.0f32); 51 | let mut smoothed_mouse = (0.0f32, 0.0f32); 52 | let mut prev_time = 0.0; 53 | let start_time = Instant::now(); 54 | let mut shapes = ShapeCache::new(); 55 | 56 | loop { 57 | let elapsed = get_elapsed(&start_time); 58 | let delta_time = elapsed - prev_time; 59 | prev_time = elapsed; 60 | 61 | events_loop.poll_events(|event| match event { 62 | glutin::Event::WindowEvent { event, .. } => match event { 63 | glutin::WindowEvent::Closed => running = false, 64 | glutin::WindowEvent::Resized(w, h) => gl_window.resize(w, h), 65 | glutin::WindowEvent::CursorMoved { position, .. } => mouse = (position.0 as f32, position.1 as f32), 66 | _ => {} 67 | }, 68 | _ => {} 69 | }); 70 | 71 | if !running { 72 | break; 73 | } 74 | 75 | let (width, height) = gl_window.get_inner_size().unwrap(); 76 | let (width, height) = (width as i32, height as i32); 77 | 78 | unsafe { 79 | gl::Viewport(0, 0, width, height); 80 | gl::ClearColor(0.3, 0.3, 0.32, 1.0); 81 | gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT | gl::STENCIL_BUFFER_BIT); 82 | } 83 | 84 | let (width, height) = (width as f32, height as f32); 85 | smoothed_mouse = smooth_mouse(mouse, smoothed_mouse, delta_time, 7.0); 86 | 87 | context.frame((width, height), gl_window.hidpi_factor(), |frame| { 88 | let block_size = 75.0; 89 | let offset = block_size / 2.0; 90 | 91 | // background 92 | render_rectangle(&frame, (0.0, 0.0), (width, height), Color::from_rgb(0xFF, 0xFF, 0xAF)); 93 | 94 | let max_cols = (width / block_size) as u16 + 2; 95 | let max_rows = (height / block_size) as u16 + 2; 96 | 97 | for x in 0..max_cols { 98 | for y in 0..max_rows { 99 | let shape = shapes.get((x, y), &mut rng); 100 | shape.update(delta_time); 101 | let x = x as f32 * block_size - offset; 102 | let y = y as f32 * block_size - offset; 103 | shape.draw(&frame, (x, y), block_size); 104 | } 105 | } 106 | 107 | render_cutout(&frame, (0.0, 0.0), (width, height), smoothed_mouse); 108 | }); 109 | 110 | gl_window.swap_buffers().unwrap(); 111 | } 112 | } 113 | 114 | fn smooth_mouse(mouse: (f32, f32), prev_smoothed_mouse: (f32, f32), dt: f32, speed: f32) -> (f32, f32) { 115 | let smx = lerp(prev_smoothed_mouse.0, mouse.0, dt * speed); 116 | let smy = lerp(prev_smoothed_mouse.1, mouse.1, dt * speed); 117 | (smx, smy) 118 | } 119 | 120 | /// Holds Shapes and generates them as needed. 121 | struct ShapeCache(HashMap); 122 | 123 | impl ShapeCache { 124 | /// Creates new ShapeCache 125 | fn new() -> ShapeCache { 126 | ShapeCache(HashMap::new()) 127 | } 128 | 129 | /// Get shape with position stored in 'pair'. 130 | /// If Shape at this position does not exists, it gets created and then returned, 131 | /// Otherwise, if Shape at this position exists, it gets returned. 132 | fn get(&mut self, pair: (u16, u16), rng: &mut T) -> &mut Shape { 133 | let index = ShapeCache::elegent_pair(pair); 134 | self.0.entry(index).or_insert_with(|| Shape::new(rng)) 135 | } 136 | 137 | /// Pairs position into one bigger integer. 138 | fn elegent_pair((x, y): (u16, u16)) -> u32 { 139 | let a = x as u32; 140 | let b = y as u32; 141 | 142 | if a >= b { 143 | a * a + a + b 144 | } else { 145 | a + b * b 146 | } 147 | } 148 | } 149 | 150 | /// All possible kinds of shapes that we can draw. 151 | enum ShapeKind { 152 | /// Polygon with number of sides to draw. 153 | /// Should be bigger or equal to 3. 154 | Polygon(u8), 155 | /// Squiggle with number of peaks to draw. 156 | Squiggle(u8), 157 | } 158 | 159 | /// Implements rand for ShapeKind, so we can easily 160 | /// generate random kinds of shapes with their random properties. 161 | impl Rand for ShapeKind { 162 | fn rand(rng: &mut R) -> Self { 163 | match rng.gen_range(0, 2) { 164 | 0 => ShapeKind::Polygon(rng.gen_range(3, 6)), 165 | 1 => ShapeKind::Squiggle(rng.gen_range(3, 6)), 166 | _ => unreachable!(), 167 | } 168 | } 169 | } 170 | 171 | /// Shape struct holding data for rendering given shape. 172 | struct Shape { 173 | /// Current rotation in radians. 174 | rotation: f32, 175 | // Current speed of rotation, can be negative (rotates in opposite direction) 176 | speed: f32, 177 | /// Color of the shape. 178 | color: Color, 179 | /// Describes how to render shape. 180 | kind: ShapeKind, 181 | } 182 | 183 | impl Shape { 184 | /// Creates new random shape. 185 | fn new(rng: &mut T) -> Shape { 186 | let color = rng.choose(&*COLORS).unwrap(); 187 | let direction = rng.choose(&[-1.0f32, 1.0f32]).unwrap(); 188 | 189 | Shape { 190 | rotation: rng.gen_range(0.0, 2.0 * PI), 191 | speed: rng.gen_range(1.0, 4.0) * direction, 192 | color: *color, 193 | kind: ShapeKind::rand(rng), 194 | } 195 | } 196 | 197 | /// Updates properties of this shape. 198 | fn update(&mut self, dt: f32) { 199 | self.rotation = self.rotation + dt * self.speed; 200 | } 201 | 202 | // Draws shape onto 'frame' at specified position, with available square area with side length of 'size'. 203 | fn draw(&self, frame: &Frame, (x, y): (f32, f32), size: f32) { 204 | let margin = size * 0.2; 205 | let x = x + margin; 206 | let y = y + margin; 207 | let size = size - margin * 2.0; 208 | let half_size = size / 2.0; 209 | let pos = (x + half_size, y + half_size); 210 | match self.kind { 211 | ShapeKind::Polygon(sides) => Shape::render_polygon(frame, pos, size, self.rotation, self.color, sides), 212 | ShapeKind::Squiggle(phi) => { 213 | Shape::render_squiggle(frame, pos, (size, size / 3.0), self.rotation, self.color, phi) 214 | } 215 | }; 216 | } 217 | 218 | /// Renders polygon at center '(cx, cy)' that is inside circle of diameter 'diameter'. 219 | /// 'color' The colors of the shape. 220 | /// 'rotation' Specifies the rotation of shape in radians. 221 | /// 'num_sides' Number of polygon sides. 222 | fn render_polygon(frame: &Frame, (cx, cy): (f32, f32), diameter: f32, rotation: f32, color: Color, num_sides: u8) { 223 | assert!(num_sides >= 3); 224 | 225 | let radius = diameter / 2.0; 226 | let num_sides = num_sides as u32; 227 | 228 | frame.path( 229 | |path| { 230 | path.move_to(Shape::get_polygon_point(0, num_sides, radius)); 231 | for i in 1..num_sides { 232 | path.line_to(Shape::get_polygon_point(i, num_sides, radius)); 233 | } 234 | path.close(); 235 | path.fill(color, Default::default()); 236 | }, 237 | PathOptions { 238 | transform: Some(Transform::new().with_translation(cx, cy).rotate(rotation)), 239 | ..Default::default() 240 | }, 241 | ); 242 | } 243 | 244 | /// Renders squiggly line at center '(cx, cy)' that is inside rectangle '(w, h)'. 245 | /// 'color' The colors of the shape. 246 | /// 'rotation' Specifies the rotation of shape in radians. 247 | /// 'phi' Specifies number of peaks (oscillations). 248 | fn render_squiggle(frame: &Frame, (cx, cy): (f32, f32), (w, h): (f32, f32), rotation: f32, color: Color, phi: u8) { 249 | let phi = phi as f32; 250 | let mut points = [(0.0, 0.0); 64]; 251 | for i in 0..points.len() { 252 | let pct = i as f32 / (points.len() as f32 - 1.0); 253 | let theta = pct * PI * 2.0 * phi + PI / 2.0; 254 | let sx = w * pct - w / 2.0; 255 | let sy = h / 2.0 * theta.sin(); 256 | points[i as usize] = (sx, sy); 257 | } 258 | 259 | frame.path( 260 | |path| { 261 | path.move_to(points[0]); 262 | for point in points.iter().skip(1) { 263 | path.line_to(*point); 264 | } 265 | path.stroke( 266 | color, 267 | StrokeOptions { 268 | width: 3.0, 269 | ..Default::default() 270 | }, 271 | ); 272 | }, 273 | PathOptions { 274 | transform: Some(Transform::new().with_translation(cx, cy).rotate(rotation)), 275 | ..Default::default() 276 | }, 277 | ); 278 | } 279 | 280 | /// Get coordinate of point in polygon at index. 281 | /// 'index' The index of point in polygon. 282 | /// 'num_sides' Specifies how many sides does polygon have. 283 | /// 'radius' The radius of polygon. 284 | fn get_polygon_point(index: u32, num_sides: u32, radius: f32) -> (f32, f32) { 285 | let px = radius * (2.0 * PI * index as f32 / num_sides as f32).cos(); 286 | let py = radius * (2.0 * PI * index as f32 / num_sides as f32).sin(); 287 | (px, py) 288 | } 289 | } 290 | 291 | /// Linearly interpolates between 'from' to 'to' by 't'. 292 | fn lerp(from: f32, to: f32, t: f32) -> f32 { 293 | from + (to - from) * t 294 | } 295 | 296 | /// Returns elased time starting from 'instant'. 297 | fn get_elapsed(instant: &Instant) -> f32 { 298 | let elapsed = instant.elapsed(); 299 | let elapsed = elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9; 300 | elapsed as f32 301 | } 302 | 303 | /// Renders foreground with hole cut in it. 304 | fn render_cutout(frame: &Frame, (x, y): (f32, f32), (w, h): (f32, f32), (mx, my): (f32, f32)) { 305 | let base_circle_size = 200.0; 306 | let circle_thickness = 25.0; 307 | frame.path( 308 | |path| { 309 | path.rect((x, y), (w, h)); 310 | path.move_to((0.0, 0.0)); 311 | path.circle((mx, my), base_circle_size); 312 | path.winding(Winding::Solidity(Solidity::Hole)); 313 | path.close(); 314 | path.fill(Color::from_rgba(255, 255, 255, 255), Default::default()); 315 | }, 316 | Default::default(), 317 | ); 318 | 319 | frame.path( 320 | |path| { 321 | path.move_to((0.0, 0.0)); 322 | path.circle((mx, my), base_circle_size + circle_thickness); 323 | path.circle((mx, my), base_circle_size); 324 | path.winding(Winding::Solidity(Solidity::Hole)); 325 | path.close(); 326 | path.fill(Color::from_rgba(90, 94, 100, 25), Default::default()); 327 | }, 328 | Default::default(), 329 | ); 330 | 331 | frame.path( 332 | |path| { 333 | path.move_to((0.0, 0.0)); 334 | path.circle((mx, my), base_circle_size); 335 | path.circle((mx, my), base_circle_size - circle_thickness); 336 | path.winding(Winding::Solidity(Solidity::Hole)); 337 | path.close(); 338 | 339 | path.fill(Color::from_rgba(0, 0, 0, 25), Default::default()); 340 | }, 341 | Default::default(), 342 | ); 343 | } 344 | 345 | /// Renders rectangle on position with specified dimensions and color. 346 | fn render_rectangle(frame: &Frame, (x, y): (f32, f32), (w, h): (f32, f32), color: Color) { 347 | frame.path( 348 | |path| { 349 | path.rect((x, y), (w, h)); 350 | path.fill(color, Default::default()); 351 | }, 352 | Default::default(), 353 | ); 354 | } 355 | -------------------------------------------------------------------------------- /examples/demo-glutin.rs: -------------------------------------------------------------------------------- 1 | extern crate gl; 2 | extern crate glutin; 3 | extern crate nanovg; 4 | 5 | use glutin::GlContext; 6 | use nanovg::{ 7 | Alignment, BasicCompositeOperation, Clip, Color, CompositeOperation, Font, Gradient, Image, ImagePattern, 8 | PathOptions, Scissor, StrokeOptions, TextOptions, Transform, 9 | }; 10 | use std::f32::consts::PI; 11 | use std::time::Instant; 12 | 13 | const INIT_WINDOW_SIZE: (u32, u32) = (1024, 720); 14 | 15 | fn main() { 16 | let mut events_loop = glutin::EventsLoop::new(); 17 | let window = glutin::WindowBuilder::new() 18 | .with_title("Glutin NanoVG") 19 | .with_dimensions(INIT_WINDOW_SIZE.0, INIT_WINDOW_SIZE.1); 20 | let context = glutin::ContextBuilder::new() 21 | .with_vsync(true) 22 | .with_multisampling(4) 23 | .with_srgb(true); 24 | let gl_window = glutin::GlWindow::new(window, context, &events_loop).unwrap(); 25 | 26 | unsafe { 27 | gl_window.make_current().unwrap(); 28 | gl::load_with(|symbol| gl_window.get_proc_address(symbol) as *const _); 29 | gl::ClearColor(0.0, 0.0, 0.0, 1.0); 30 | } 31 | 32 | let context = nanovg::ContextBuilder::new() 33 | .stencil_strokes() 34 | .build() 35 | .expect("Initialization of NanoVG failed!"); 36 | let img = Image::new(&context) 37 | .repeat_x() 38 | .repeat_y() 39 | .build_from_file("resources/lenna.png") 40 | .expect("Couldn't load image"); 41 | let mechanic_font = Font::from_file(&context, "Mechanic", "resources/Mechanic of the Heart.ttf") 42 | .expect("Failed to load font 'Mechanic of the Heart.ttf'"); 43 | 44 | let start_time = Instant::now(); 45 | let mut running = true; 46 | 47 | while running { 48 | events_loop.poll_events(|event| match event { 49 | glutin::Event::WindowEvent { event, .. } => match event { 50 | glutin::WindowEvent::Closed => running = false, 51 | glutin::WindowEvent::Resized(w, h) => gl_window.resize(w, h), 52 | _ => {} 53 | }, 54 | _ => {} 55 | }); 56 | 57 | let (width, height) = gl_window.get_inner_size().unwrap(); 58 | let (width, height) = (width as i32, height as i32); 59 | 60 | unsafe { 61 | gl::Viewport(0, 0, width, height); 62 | gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT | gl::STENCIL_BUFFER_BIT); 63 | } 64 | 65 | let (width, height) = (width as f32, height as f32); 66 | let elapsed = { 67 | let elapsed = start_time.elapsed(); 68 | elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9 69 | } as f32; 70 | 71 | // Let's draw a frame! 72 | 73 | context.frame((width, height), gl_window.hidpi_factor(), |frame| { 74 | // Draw red-filled rectangle. 75 | frame.path( 76 | |path| { 77 | path.rect((100.0, 100.0), (300.0, 300.0)); 78 | path.fill( 79 | Gradient::Linear { 80 | start: (100.0, 100.0), 81 | end: (400.0, 400.0), 82 | start_color: Color::from_rgb(0xAA, 0x6C, 0x39), 83 | end_color: Color::from_rgb(0x88, 0x2D, 0x60), 84 | }, 85 | Default::default(), 86 | ); 87 | }, 88 | Default::default(), 89 | ); 90 | 91 | // Draw custom yellow/blue shape. 92 | frame.path( 93 | |path| { 94 | let origin = (150.0, 140.0); 95 | path.circle(origin, 64.0); 96 | path.move_to(origin); 97 | path.line_to((origin.0 + 300.0, origin.1 - 50.0)); 98 | path.quad_bezier_to((origin.0 + 500.0, origin.1 + 100.0), (300.0, 100.0)); 99 | path.close(); 100 | path.stroke( 101 | Color::new(1.0, 1.0, 0.0, 1.0), 102 | StrokeOptions { 103 | width: 3.0, 104 | ..Default::default() 105 | }, 106 | ); 107 | path.fill(Color::new(0.2, 0.0, 0.8, 1.0), Default::default()); 108 | }, 109 | PathOptions { 110 | composite_operation: CompositeOperation::Basic(BasicCompositeOperation::Lighter), 111 | alpha: elapsed.cos() * 0.5 + 0.5, 112 | transform: Some(Transform::new().translate(100.0, 50.0).scale(0.2, 1.0)), 113 | ..Default::default() 114 | }, 115 | ); 116 | 117 | // Draw rolling image (with scissors) 118 | frame.path( 119 | |path| { 120 | let radius = 100.0; 121 | let distance = 500.0; // Distance to roll 122 | let rolled = ((elapsed / 5.0).sin() * 0.5 + 0.5) * distance; // Distance currently rolled 123 | let origin = (rolled + 100.0, 600.0); 124 | let paint = ImagePattern { 125 | image: &img, 126 | origin: origin, 127 | size: (100.0, 100.0), 128 | angle: rolled / (2.0 * PI * radius) * 2.0 * PI, 129 | alpha: 1.0, 130 | }; 131 | path.circle(origin, radius); 132 | path.fill(paint, Default::default()); 133 | }, 134 | PathOptions { 135 | clip: Clip::Scissor(Scissor { 136 | x: 150.0, 137 | y: 600.0, 138 | width: 1000.0, 139 | height: 200.0, 140 | transform: None, 141 | }), 142 | ..Default::default() 143 | }, 144 | ); 145 | 146 | // Draw stroked rectangle. 147 | frame.path( 148 | |path| { 149 | path.rect((300.0, 310.0), (300.0, 300.0)); 150 | let color = Color::lerp( 151 | Color::from_rgb(0x2e, 0x50, 0x77), 152 | Color::from_rgb(0xff, 0xca, 0x77), 153 | elapsed.sin() * 0.5 + 0.5, 154 | ); 155 | path.fill(Color::new(0.2, 0.2, 0.2, 0.7), Default::default()); 156 | path.stroke( 157 | color, 158 | StrokeOptions { 159 | width: 5.0, 160 | ..Default::default() 161 | }, 162 | ); 163 | }, 164 | Default::default(), 165 | ); 166 | 167 | // Draw some strings! 168 | 169 | frame.text( 170 | mechanic_font, 171 | (50.0, 50.0), 172 | "Hello world", 173 | TextOptions { 174 | color: Color::new(1.0, 1.0, 1.0, 1.0), 175 | size: 24.0, 176 | letter_spacing: (elapsed.sin() * 0.5 + 0.5) * 30.0, 177 | ..Default::default() 178 | }, 179 | ); 180 | 181 | frame.text_box( 182 | mechanic_font, 183 | (50.0, 74.0), 184 | "Multi-\nline", 185 | TextOptions { 186 | color: Color::new(1.0, 0.6, 1.0, 1.0), 187 | size: 24.0, 188 | ..Default::default() 189 | }, 190 | ); 191 | 192 | frame.text_box( 193 | mechanic_font, 194 | (800.0, 50.0), 195 | "This text is automatically wrapped.\nResize the window and try it out!", 196 | TextOptions { 197 | color: Color::new(0.6, 1.0, 1.0, 1.0), 198 | size: 24.0, 199 | align: Alignment::new().right().baseline(), 200 | line_height: 1.2, 201 | line_max_width: gl_window.get_inner_size().unwrap_or(INIT_WINDOW_SIZE).0 as f32 - 800.0, 202 | ..Default::default() 203 | }, 204 | ); 205 | }); 206 | 207 | gl_window.swap_buffers().unwrap(); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /examples/demo-text.rs: -------------------------------------------------------------------------------- 1 | extern crate gl; 2 | extern crate glutin; 3 | extern crate nanovg; 4 | 5 | use glutin::GlContext; 6 | use nanovg::{Alignment, Clip, Color, Font, Frame, PathOptions, Scissor, TextOptions, Transform}; 7 | use std::f32::consts::PI; 8 | use std::time::Instant; 9 | 10 | const INIT_WINDOW_SIZE: (u32, u32) = (300, 300); 11 | 12 | fn main() { 13 | let mut events_loop = glutin::EventsLoop::new(); 14 | let window = glutin::WindowBuilder::new() 15 | .with_title("NanoVG Text") 16 | .with_dimensions(INIT_WINDOW_SIZE.0, INIT_WINDOW_SIZE.1); 17 | let context = glutin::ContextBuilder::new() 18 | .with_vsync(false) 19 | .with_multisampling(4) 20 | .with_srgb(true); 21 | let gl_window = glutin::GlWindow::new(window, context, &events_loop).unwrap(); 22 | 23 | unsafe { 24 | gl_window.make_current().unwrap(); 25 | gl::load_with(|symbol| gl_window.get_proc_address(symbol) as *const _); 26 | } 27 | 28 | let context = nanovg::ContextBuilder::new() 29 | .stencil_strokes() 30 | .build() 31 | .expect("Initialization of NanoVG failed!"); 32 | 33 | let font = Font::from_file(&context, "Roboto-Regular", "resources/Roboto-Regular.ttf") 34 | .expect("Failed to load font 'Roboto-Regular.ttf'"); 35 | 36 | let emoji = Font::from_file(&context, "NotoEmoji", "resources/NotoEmoji-Regular.ttf") 37 | .expect("Failed to load font 'NotoEmoji-Regular.ttf'"); 38 | 39 | font.add_fallback(emoji); 40 | 41 | let mut running = true; 42 | let mut mouse = (0.0f32, 0.0f32); 43 | let start_time = Instant::now(); 44 | 45 | loop { 46 | let elapsed = get_elapsed(&start_time); 47 | events_loop.poll_events(|event| match event { 48 | glutin::Event::WindowEvent { event, .. } => match event { 49 | glutin::WindowEvent::Closed => running = false, 50 | glutin::WindowEvent::Resized(w, h) => gl_window.resize(w, h), 51 | glutin::WindowEvent::CursorMoved { position, .. } => mouse = (position.0 as f32, position.1 as f32), 52 | _ => {} 53 | }, 54 | _ => {} 55 | }); 56 | 57 | if !running { 58 | break; 59 | } 60 | 61 | let (width, height) = gl_window.get_inner_size().unwrap(); 62 | let (width, height) = (width as i32, height as i32); 63 | 64 | unsafe { 65 | gl::Viewport(0, 0, width, height); 66 | gl::ClearColor(0.3, 0.3, 0.32, 1.0); 67 | gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT | gl::STENCIL_BUFFER_BIT); 68 | } 69 | 70 | let (width, height) = (width as f32, height as f32); 71 | context.frame((width, height), gl_window.hidpi_factor(), |frame| { 72 | let margin = 50.0; 73 | let clip = (margin, margin, width - margin * 2.0, height - margin * 2.0); 74 | let transform = Transform::new() 75 | .with_translation(mouse.0, mouse.1) 76 | .rotate(elapsed * 4.0); 77 | render_text(&frame, font, "text", clip, transform); 78 | let transform = Transform::new().with_translation(150.0, 100.0).rotate(-PI / 6.0); 79 | draw_paragraph(&frame, font, -150.0 / 2.0, -50.0, 150.0, 100.0, mouse, transform); 80 | }); 81 | 82 | gl_window.swap_buffers().unwrap(); 83 | } 84 | } 85 | 86 | fn get_elapsed(instant: &Instant) -> f32 { 87 | let elapsed = instant.elapsed(); 88 | let elapsed = elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9; 89 | elapsed as f32 90 | } 91 | 92 | fn render_text(frame: &Frame, font: Font, text: &str, clip: (f32, f32, f32, f32), transform: Transform) { 93 | let (cx, cy, cw, ch) = clip; 94 | let scissor = Scissor { 95 | x: cx, 96 | y: cy, 97 | width: cw, 98 | height: ch, 99 | transform: None, 100 | }; 101 | 102 | // draw clipping area 103 | frame.path( 104 | |path| { 105 | path.rect((cx, cy), (cw, ch)); 106 | path.stroke(Color::from_rgb(0, 0, 0), Default::default()); 107 | }, 108 | PathOptions::default(), 109 | ); 110 | 111 | // draw small rectangle that is translated with transform 112 | // and gets clipped into clipping area 113 | frame.path( 114 | |path| { 115 | path.rect((-50.0, -50.0), (50.0, 50.0)); 116 | path.fill(Color::from_rgb(50, 50, 50), Default::default()); 117 | }, 118 | PathOptions { 119 | clip: Clip::Scissor(scissor), 120 | transform: Some(transform), 121 | ..Default::default() 122 | }, 123 | ); 124 | 125 | frame.text( 126 | font, 127 | (0.0, 0.0), 128 | text, 129 | TextOptions { 130 | size: 28.0, 131 | color: Color::from_rgb(255, 255, 255), 132 | align: Alignment::new().bottom().right(), 133 | clip: Clip::Scissor(scissor), 134 | transform: Some(transform), 135 | ..Default::default() 136 | }, 137 | ); 138 | } 139 | 140 | fn clamp(value: f32, min: f32, max: f32) -> f32 { 141 | if value < min { 142 | min 143 | } else if value > max { 144 | max 145 | } else { 146 | value 147 | } 148 | } 149 | 150 | fn draw_paragraph( 151 | frame: &Frame, 152 | font: Font, 153 | x: f32, 154 | y: f32, 155 | width: f32, 156 | _height: f32, 157 | mouse: (f32, f32), 158 | transform: Transform, 159 | ) { 160 | let text = "This is longer chunk of text.\n \n Would have used lorem ipsum but she was busy jumping over the lazy dog with the fox and all the men who came to the aid of the party.🎉"; 161 | let text_options = TextOptions { 162 | color: Color::from_rgba(255, 255, 255, 255), 163 | size: 18.0, 164 | align: Alignment::new().left().top(), 165 | transform: Some(transform), 166 | ..Default::default() 167 | }; 168 | let metrics = frame.text_metrics(font, text_options); 169 | 170 | let mut y = y; 171 | let (mx, my) = if let Some(inv) = transform.try_inverse() { 172 | inv.transform_point(mouse) 173 | } else { 174 | mouse 175 | }; 176 | 177 | for row in frame.text_break_lines(font, text, width, text_options) { 178 | let hit = mx > x && mx < (x + width) && my >= y && my < (y + metrics.line_height); 179 | 180 | // draw line background 181 | frame.path( 182 | |path| { 183 | path.rect((x, y), (row.width, metrics.line_height)); 184 | path.fill( 185 | Color::from_rgba(255, 255, 255, if hit { 64 } else { 16 }), 186 | Default::default(), 187 | ); 188 | }, 189 | PathOptions { 190 | transform: Some(transform), 191 | ..Default::default() 192 | }, 193 | ); 194 | 195 | // draw line text 196 | frame.text(font, (x, y), row.text, text_options); 197 | 198 | if hit { 199 | let mut caretx = if mx < x + row.width / 2.0 { x } else { x + row.width }; 200 | let mut px = x; 201 | 202 | // calculate mouse caret position 203 | let mut glyph_positions = frame.text_glyph_positions((x, y), row.text).peekable(); 204 | while let Some(glyph) = glyph_positions.next() { 205 | let x0 = glyph.x; 206 | let x1 = if let Some(next) = glyph_positions.peek() { 207 | next.x 208 | } else { 209 | x + row.width 210 | }; 211 | let gx = x0 * 0.3 + x1 * 0.7; 212 | 213 | if mx >= px && mx < gx { 214 | caretx = x0; 215 | break; 216 | } 217 | 218 | px = gx; 219 | } 220 | 221 | // draw mouse caret 222 | frame.path( 223 | |path| { 224 | path.rect((caretx, y), (1.0, metrics.line_height)); 225 | path.fill(Color::from_rgba(255, 192, 0, 255), Default::default()); 226 | }, 227 | PathOptions { 228 | transform: Some(transform), 229 | ..Default::default() 230 | }, 231 | ); 232 | } 233 | 234 | y += metrics.line_height; 235 | } 236 | 237 | draw_tooltip(frame, (x, y + 20.0), mouse, font, transform.clone()); 238 | } 239 | 240 | fn draw_tooltip(frame: &Frame, (x, y): (f32, f32), mouse: (f32, f32), font: Font, transform: Transform) { 241 | let tooltip_text = "Hover your mouse over the text to see calculated caret position."; 242 | let tooltip_opts = TextOptions { 243 | color: Color::from_rgba(0, 0, 0, 220), 244 | size: 13.0, 245 | align: Alignment::new().left().top(), 246 | line_height: 1.2, 247 | line_max_width: 150.0, 248 | transform: Some(transform), 249 | ..Default::default() 250 | }; 251 | // draw tooltip 252 | let bounds = frame.text_box_bounds(font, (x, y), tooltip_text, tooltip_opts); 253 | let (mx, my) = if let Some(inv) = transform.try_inverse() { 254 | inv.transform_point(mouse) 255 | } else { 256 | mouse 257 | }; 258 | 259 | let gx = f32::abs((mx - (bounds.min_x + bounds.max_x) * 0.5) / (bounds.min_x - bounds.max_x)); 260 | let gy = f32::abs((my - (bounds.min_y + bounds.max_y) * 0.5) / (bounds.min_y - bounds.max_y)); 261 | let alpha = f32::max(gx, gy) - 0.5; 262 | let alpha = clamp(alpha, 0.0, 1.0); 263 | 264 | frame.path( 265 | |path| { 266 | path.rounded_rect( 267 | (bounds.min_x - 2.0, bounds.min_y - 2.0), 268 | (bounds.max_x - bounds.min_x + 4.0, bounds.max_y - bounds.min_y + 4.0), 269 | 3.0, 270 | ); 271 | let px = (bounds.max_x + bounds.min_x) / 2.0; 272 | let py = bounds.min_y; 273 | path.move_to((px, py - 10.0)); 274 | path.line_to((px + 7.0, py + 1.0)); 275 | path.line_to((px - 7.0, py + 1.0)); 276 | path.fill(Color::from_rgba(220, 220, 220, 255), Default::default()); 277 | }, 278 | PathOptions { 279 | alpha, 280 | transform: Some(transform), 281 | ..Default::default() 282 | }, 283 | ); 284 | 285 | frame.text_box(font, (x, y), tooltip_text, tooltip_opts); 286 | } 287 | -------------------------------------------------------------------------------- /examples/demo-transform.rs: -------------------------------------------------------------------------------- 1 | extern crate gl; 2 | extern crate glutin; 3 | extern crate nanovg; 4 | 5 | use glutin::GlContext; 6 | use nanovg::{ 7 | Alignment, Clip, Color, Font, Frame, Gradient, PathOptions, Scissor, StrokeOptions, TextOptions, Transform, 8 | }; 9 | use std::time::Instant; 10 | 11 | const INIT_WINDOW_SIZE: (u32, u32) = (300, 300); 12 | 13 | fn main() { 14 | let mut events_loop = glutin::EventsLoop::new(); 15 | let window = glutin::WindowBuilder::new() 16 | .with_title("NanoVG Transform") 17 | .with_dimensions(INIT_WINDOW_SIZE.0, INIT_WINDOW_SIZE.1); 18 | let context = glutin::ContextBuilder::new() 19 | .with_vsync(false) 20 | .with_multisampling(4) 21 | .with_srgb(true); 22 | let gl_window = glutin::GlWindow::new(window, context, &events_loop).unwrap(); 23 | 24 | unsafe { 25 | gl_window.make_current().unwrap(); 26 | gl::load_with(|symbol| gl_window.get_proc_address(symbol) as *const _); 27 | } 28 | 29 | let context = nanovg::ContextBuilder::new() 30 | .stencil_strokes() 31 | .build() 32 | .expect("Initialization of NanoVG failed!"); 33 | 34 | let font = Font::from_file(&context, "Roboto-Regular", "resources/Roboto-Regular.ttf") 35 | .expect("Failed to load font 'Roboto-Regular.ttf'"); 36 | 37 | let emoji = Font::from_file(&context, "NotoEmoji", "resources/NotoEmoji-Regular.ttf") 38 | .expect("Failed to load font 'NotoEmoji-Regular.ttf'"); 39 | 40 | font.add_fallback(emoji); 41 | 42 | let mut running = true; 43 | let mut mouse = (0.0f32, 0.0f32); 44 | let start_time = Instant::now(); 45 | 46 | loop { 47 | let elapsed = get_elapsed(&start_time); 48 | events_loop.poll_events(|event| match event { 49 | glutin::Event::WindowEvent { event, .. } => match event { 50 | glutin::WindowEvent::Closed => running = false, 51 | glutin::WindowEvent::Resized(w, h) => gl_window.resize(w, h), 52 | glutin::WindowEvent::CursorMoved { position, .. } => mouse = (position.0 as f32, position.1 as f32), 53 | _ => {} 54 | }, 55 | _ => {} 56 | }); 57 | 58 | if !running { 59 | break; 60 | } 61 | 62 | let (width, height) = gl_window.get_inner_size().unwrap(); 63 | let (width, height) = (width as i32, height as i32); 64 | 65 | unsafe { 66 | gl::Viewport(0, 0, width, height); 67 | gl::ClearColor(0.3, 0.3, 0.32, 1.0); 68 | gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT | gl::STENCIL_BUFFER_BIT); 69 | } 70 | 71 | let (width, height) = (width as f32, height as f32); 72 | context.frame((width, height), gl_window.hidpi_factor(), |mut frame| { 73 | let (width, height) = (width as f32, height as f32); 74 | 75 | // position red rect that drawn rotated at offset with transform 76 | frame.transformed(Transform::new().translate(width / 2.0, height + 100.0), |frame| { 77 | rotating_red_rect(&frame, 50.0, 50.0, elapsed); 78 | }); 79 | 80 | // position button with x and y 81 | draw_button( 82 | &frame, 83 | font, 84 | "Button", 85 | 20.0, 86 | 150.0, 87 | 100.0, 88 | 40.0, 89 | Color::from_rgb(64, 64, 64), 90 | ); 91 | 92 | // position multiple buttons with transform 93 | frame.transformed(Transform::new().translate(width - 125.0, 20.0), |frame| { 94 | button_container(&frame, font, 125.0); 95 | }); 96 | 97 | let translate = Transform::new().translate(60.0, 60.0); 98 | let rotate = Transform::new().rotate(elapsed); 99 | 100 | // here frame gets translated with transform, 101 | // green rectangle gets drawn, and then it gets rotated in PathOptions 102 | frame.transformed(translate, |frame| { 103 | frame.path( 104 | |path| { 105 | path.rect((0.0, 0.0), (50.0, 50.0)); 106 | path.fill(Color::from_rgb(0, 255, 0), Default::default()); 107 | }, 108 | PathOptions { 109 | transform: Some(rotate), 110 | ..Default::default() 111 | }, 112 | ); 113 | }); 114 | 115 | let transform = rotate * translate; // we can multiply or premultiply Transforms 116 | 117 | // this has same effect as the one above 118 | // only draws white stroke on the green rectangle drawn above 119 | frame.path( 120 | |path| { 121 | path.rect((0.0, 0.0), (50.0, 50.0)); 122 | path.stroke( 123 | Color::from_rgb(255, 255, 255), 124 | StrokeOptions { 125 | width: 5.0, 126 | ..Default::default() 127 | }, 128 | ); 129 | }, 130 | PathOptions { 131 | transform: Some(transform), 132 | ..Default::default() 133 | }, 134 | ); 135 | 136 | // this example renders rotated rectangular 137 | // area where rectangle gets drawn and rotated 138 | // at mouse position, when rectangle gets 139 | // out of this area it gets clipped 140 | let margin = 50.0; 141 | let clip = (margin, margin, width - margin * 2.0, height - margin * 2.0); 142 | let mouse_transform = Transform::new() 143 | .with_translation(mouse.0, mouse.1) 144 | .rotate(elapsed * 4.0); 145 | frame.transformed(Transform::new().rotate(10.0f32.to_radians()), |frame| { 146 | render_area(&frame, font, clip, mouse_transform.absolute()); // the absolute is here because we do not want 147 | // our mouse to be translated in frame's local coordinate space, 148 | // we want to use it as it is 149 | // if you we to remove it, the rectangle inside area that is 150 | // dragged by mouse would get invalid coordinates 151 | }); 152 | }); 153 | 154 | gl_window.swap_buffers().unwrap(); 155 | } 156 | } 157 | 158 | fn get_elapsed(instant: &Instant) -> f32 { 159 | let elapsed = instant.elapsed(); 160 | let elapsed = elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9; 161 | elapsed as f32 162 | } 163 | 164 | fn rotating_red_rect(frame: &Frame, w: f32, h: f32, t: f32) { 165 | frame.path( 166 | |path| { 167 | path.rect((-w / 2.0, -h / 2.0), (w, h)); 168 | path.fill(Color::from_rgb(255, 0, 0), Default::default()); 169 | }, 170 | PathOptions { 171 | transform: Some(Transform::new().rotate(t * 2.0)), 172 | ..Default::default() 173 | }, 174 | ); 175 | } 176 | 177 | fn is_black(color: Color) -> bool { 178 | color.red() == 0.0 && color.green() == 0.0 && color.blue() == 0.0 && color.alpha() == 0.0 179 | } 180 | 181 | fn button_container(frame: &Frame, font: Font, w: f32) { 182 | draw_button(frame, font, "Button 1", 0.0, 0.0, w, 35.0, Color::from_rgb(123, 0, 0)); 183 | draw_button(frame, font, "Button 2", 0.0, 50.0, w, 35.0, Color::from_rgb(0, 123, 0)); 184 | draw_button(frame, font, "Button 3", 0.0, 100.0, w, 35.0, Color::from_rgb(0, 0, 123)); 185 | } 186 | 187 | fn draw_button(frame: &Frame, font: Font, text: &str, x: f32, y: f32, w: f32, h: f32, color: Color) { 188 | let corner_radius = 4.0; 189 | let color_is_black = is_black(color); 190 | 191 | // button background 192 | frame.path( 193 | |path| { 194 | path.rounded_rect((x + 1.0, y + 1.0), (w - 2.0, h - 2.0), corner_radius - 0.5); 195 | if !color_is_black { 196 | path.fill(color, Default::default()); 197 | } 198 | 199 | path.fill( 200 | Gradient::Linear { 201 | start: (x, y), 202 | end: (w, h), 203 | start_color: Color::from_rgba(255, 255, 255, if color_is_black { 16 } else { 32 }), 204 | end_color: Color::from_rgba(0, 0, 0, if color_is_black { 16 } else { 32 }), 205 | }, 206 | Default::default(), 207 | ); 208 | }, 209 | Default::default(), 210 | ); 211 | 212 | // button border 213 | frame.path( 214 | |path| { 215 | path.rounded_rect((x + 0.5, y + 0.5), (w - 1.0, h - 1.0), corner_radius - 0.5); 216 | path.stroke(Color::from_rgba(0, 0, 0, 48), Default::default()); 217 | }, 218 | Default::default(), 219 | ); 220 | 221 | let mut options = TextOptions { 222 | size: 20.0, 223 | align: Alignment::new().center().middle(), 224 | ..Default::default() 225 | }; 226 | 227 | options.color = Color::from_rgba(0, 0, 0, 160); 228 | 229 | frame.text(font, (x + w / 2.0, y + h / 2.0 - 1.0), text, options); 230 | 231 | options.color = Color::from_rgba(255, 255, 255, 160); 232 | 233 | frame.text(font, (x + w / 2.0 + 0.25, y + h / 2.0), text, options); 234 | } 235 | 236 | fn render_area(frame: &Frame, font: Font, clip: (f32, f32, f32, f32), transform: Transform) { 237 | let (cx, cy, cw, ch) = clip; 238 | let scissor = Scissor { 239 | x: cx, 240 | y: cy, 241 | width: cw, 242 | height: ch, 243 | transform: None, 244 | }; 245 | 246 | // draw clipping area 247 | frame.path( 248 | |path| { 249 | path.rect((cx, cy), (cw, ch)); 250 | path.fill(Color::from_rgba(255, 255, 255, 20), Default::default()); 251 | path.stroke(Color::from_rgb(0, 0, 0), Default::default()); 252 | }, 253 | PathOptions::default(), 254 | ); 255 | 256 | // draw small rectangle that is translated with transform 257 | // and gets clipped into clipping area 258 | frame.path( 259 | |path| { 260 | path.rect((-50.0, -50.0), (50.0, 50.0)); 261 | path.fill(Color::from_rgb(50, 50, 50), Default::default()); 262 | }, 263 | PathOptions { 264 | clip: Clip::Scissor(scissor), 265 | transform: Some(transform), 266 | ..Default::default() 267 | }, 268 | ); 269 | 270 | frame.text( 271 | font, 272 | (0.0, 0.0), 273 | "text", 274 | TextOptions { 275 | size: 28.0, 276 | color: Color::from_rgb(255, 255, 255), 277 | align: Alignment::new().bottom().right(), 278 | clip: Clip::Scissor(scissor), 279 | transform: Some(transform), 280 | ..Default::default() 281 | }, 282 | ); 283 | } 284 | -------------------------------------------------------------------------------- /examples/demo-ui.rs: -------------------------------------------------------------------------------- 1 | extern crate gl; 2 | extern crate glutin; 3 | extern crate nanovg; 4 | extern crate rand; 5 | 6 | use glutin::GlContext; 7 | use nanovg::{ 8 | Alignment, Clip, Color, Context, Direction, Font, Frame, Gradient, Image, ImagePattern, Intersect, LineCap, 9 | LineJoin, PathOptions, Scissor, Solidity, StrokeOptions, TextOptions, Transform, Winding, 10 | }; 11 | use rand::Rng; 12 | use std::f32::consts; 13 | use std::time::Instant; 14 | 15 | const INIT_WINDOW_SIZE: (u32, u32) = (1000, 600); 16 | 17 | const ICON_SEARCH: &str = "\u{1F50D}"; 18 | const ICON_CIRCLED_CROSS: &str = "\u{2716}"; 19 | const ICON_CHEVRON_RIGHT: &str = "\u{E75E}"; 20 | const ICON_CHECK: &str = "\u{2713}"; 21 | const ICON_LOGIN: &str = "\u{E740}"; 22 | const ICON_TRASH: &str = "\u{E729}"; 23 | 24 | const GRAPH_HISTORY_COUNT: usize = 100; 25 | 26 | struct DemoData<'a> { 27 | fonts: DemoFonts<'a>, 28 | images: Vec>, 29 | } 30 | 31 | struct DemoFonts<'a> { 32 | icons: Font<'a>, 33 | sans: Font<'a>, 34 | sans_bold: Font<'a>, 35 | } 36 | 37 | fn main() { 38 | let mut events_loop = glutin::EventsLoop::new(); 39 | let window = glutin::WindowBuilder::new() 40 | .with_title("NanoVG UI") 41 | .with_dimensions(INIT_WINDOW_SIZE.0, INIT_WINDOW_SIZE.1); 42 | let context = glutin::ContextBuilder::new() 43 | .with_vsync(false) 44 | .with_multisampling(4) 45 | .with_srgb(true); 46 | let gl_window = glutin::GlWindow::new(window, context, &events_loop).unwrap(); 47 | 48 | unsafe { 49 | gl_window.make_current().unwrap(); 50 | gl::load_with(|symbol| gl_window.get_proc_address(symbol) as *const _); 51 | } 52 | 53 | let context = nanovg::ContextBuilder::new() 54 | .stencil_strokes() 55 | .build() 56 | .expect("Initialization of NanoVG failed!"); 57 | 58 | let start_time = Instant::now(); 59 | let mut running = true; 60 | 61 | let mut mx = 0.0f32; 62 | let mut my = 0.0f32; 63 | 64 | let demo_data = load_demo_data(&context); 65 | 66 | let mut fps_graph = PerformanceGraph::new(GraphRenderStyle::Fps, "Frame Time"); 67 | let mut cpu_graph = PerformanceGraph::new(GraphRenderStyle::Ms, "CPU Time"); 68 | let mut rng_graph = PerformanceGraph::new(GraphRenderStyle::Percent, "Random"); 69 | 70 | let mut percent = 0.0f32; 71 | let mut rng = rand::thread_rng(); 72 | let mut prev_time = 0.0; 73 | 74 | while running { 75 | let elapsed = get_elapsed(&start_time); 76 | let delta_time = elapsed - prev_time; 77 | prev_time = elapsed; 78 | 79 | events_loop.poll_events(|event| match event { 80 | glutin::Event::WindowEvent { event, .. } => match event { 81 | glutin::WindowEvent::Closed => running = false, 82 | glutin::WindowEvent::Resized(w, h) => gl_window.resize(w, h), 83 | glutin::WindowEvent::CursorMoved { position, .. } => { 84 | mx = position.0 as f32; 85 | my = position.1 as f32; 86 | } 87 | _ => {} 88 | }, 89 | _ => {} 90 | }); 91 | 92 | let (width, height) = gl_window.get_inner_size().unwrap(); 93 | let (width, height) = (width as i32, height as i32); 94 | 95 | unsafe { 96 | gl::Viewport(0, 0, width, height); 97 | gl::ClearColor(0.3, 0.3, 0.32, 1.0); 98 | gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT | gl::STENCIL_BUFFER_BIT); 99 | } 100 | 101 | let (width, height) = (width as f32, height as f32); 102 | context.frame((width, height), gl_window.hidpi_factor(), |frame| { 103 | render_demo(&frame, mx, my, width as f32, height as f32, elapsed, &demo_data); 104 | 105 | fps_graph.draw(&frame, demo_data.fonts.sans, 5.0, 5.0); 106 | cpu_graph.draw(&frame, demo_data.fonts.sans, 5.0 + 200.0 + 5.0, 5.0); 107 | rng_graph.draw(&frame, demo_data.fonts.sans, 5.0 + 200.0 + 5.0 + 200.0 + 5.0, 5.0); 108 | }); 109 | 110 | fps_graph.update(delta_time); 111 | 112 | percent = if rng.gen() { percent + 1.0 } else { percent - 1.0 }; 113 | percent = clamp(percent, 0.0, 100.0); 114 | rng_graph.update(percent); 115 | 116 | let cpu_time = get_elapsed(&start_time) - elapsed; 117 | cpu_graph.update(cpu_time); 118 | 119 | gl_window.swap_buffers().unwrap(); 120 | } 121 | 122 | println!("Average Frame Time: {:.2} ms", fps_graph.average() * 1000.0); 123 | println!(" CPU Time: {:.2} ms", cpu_graph.average() * 1000.0); 124 | println!(" RNG Percent: {:.2}% ", rng_graph.average()); 125 | } 126 | 127 | fn load_demo_data(context: &Context) -> DemoData { 128 | let demo_fonts = DemoFonts { 129 | icons: Font::from_file(context, "Entypo", "resources/entypo.ttf").expect("Failed to load font 'entypo.ttf'"), 130 | 131 | sans: Font::from_file(context, "Roboto-Regular", "resources/Roboto-Regular.ttf") 132 | .expect("Failed to load font 'Roboto-Regular.ttf'"), 133 | 134 | sans_bold: Font::from_file(context, "Roboto-Bold", "resources/Roboto-Bold.ttf") 135 | .expect("Failed to load font 'Roboto-Bold.ttf'"), 136 | }; 137 | 138 | let emoji = Font::from_file(context, "NotoEmoji", "resources/NotoEmoji-Regular.ttf") 139 | .expect("Failed to load font 'NotoEmoji-Regular.ttf'"); 140 | 141 | let mut images = Vec::new(); 142 | for i in 0..12 { 143 | let file_name = format!("resources/images/image{}.jpg", i + 1); 144 | let image = Image::new(context) 145 | .build_from_file(&file_name) 146 | .expect(&format!("Failed to load image {}", &file_name)); 147 | images.push(image); 148 | } 149 | 150 | demo_fonts.sans.add_fallback(emoji); 151 | demo_fonts.sans_bold.add_fallback(emoji); 152 | 153 | DemoData { 154 | fonts: demo_fonts, 155 | images: images, 156 | } 157 | } 158 | 159 | fn get_elapsed(instant: &Instant) -> f32 { 160 | let elapsed = instant.elapsed(); 161 | let elapsed = elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9; 162 | elapsed as f32 163 | } 164 | 165 | fn is_black(color: Color) -> bool { 166 | color.red() == 0.0 && color.green() == 0.0 && color.blue() == 0.0 && color.alpha() == 0.0 167 | } 168 | 169 | fn clamp(value: f32, min: f32, max: f32) -> f32 { 170 | if value < min { 171 | min 172 | } else if value > max { 173 | max 174 | } else { 175 | value 176 | } 177 | } 178 | 179 | fn render_demo(frame: &Frame, mx: f32, my: f32, width: f32, height: f32, t: f32, data: &DemoData) { 180 | draw_eyes(frame, width - 250.0, 50.0, 150.0, 100.0, mx, my, t); 181 | draw_paragraph(frame, &data.fonts, width - 450.0, 50.0, 150.0, 100.0, mx, my); 182 | draw_graph(frame, 0.0, height / 2.0, width, height / 2.0, t); 183 | draw_color_wheel(frame, width - 300.0, height - 300.0, 250.0, 250.0, t); 184 | draw_lines(frame, 120.0, height - 50.0, 600.0, 50.0, t); 185 | draw_widths(frame, 10.0, 50.0, 30.0); 186 | draw_caps(frame, 10.0, 300.0, 30.0); 187 | 188 | draw_scissor(&frame, 50.0, height - 80.0, t); 189 | 190 | let mut x = 50.0; 191 | let mut y = 50.0; 192 | // widgets 193 | draw_window(frame, &data.fonts, "Widgets `n stuff", x, y, 300.0, 400.0); 194 | x += 10.0; 195 | y += 45.0; 196 | 197 | draw_search_box(frame, &data.fonts, "Search", x, y, 280.0, 25.0); 198 | y += 40.0; 199 | 200 | draw_drop_down(frame, &data.fonts, "Effects", x, y, 280.0, 28.0); 201 | let popy = y + 14.0; 202 | y += 45.0; 203 | 204 | // form 205 | draw_label(frame, &data.fonts, "Login", x, y, 280.0, 20.0); 206 | y += 25.0; 207 | 208 | draw_edit_box(frame, &data.fonts, "Email", x, y, 280.0, 28.0); 209 | y += 35.0; 210 | 211 | draw_edit_box(frame, &data.fonts, "Password", x, y, 280.0, 28.0); 212 | y += 38.0; 213 | 214 | draw_check_box(frame, &data.fonts, "Remember me", x, y, 140.0, 28.0); 215 | draw_button( 216 | frame, 217 | &data.fonts, 218 | Some(ICON_LOGIN), 219 | "Sign in", 220 | x + 138.0, 221 | y, 222 | 140.0, 223 | 28.0, 224 | Color::from_rgba(0, 96, 128, 255), 225 | ); 226 | y += 45.0; 227 | 228 | // slider 229 | draw_label(frame, &data.fonts, "Diameter", x, y, 280.0, 20.0); 230 | y += 25.0; 231 | 232 | draw_edit_box_num(frame, &data.fonts, "128.00", "px", x + 180.0, y, 100.0, 28.0); 233 | 234 | draw_slider(frame, 0.4, x, y, 170.0, 28.0); 235 | y += 55.0; 236 | 237 | draw_button( 238 | frame, 239 | &data.fonts, 240 | Some(ICON_TRASH), 241 | "Delete", 242 | x, 243 | y, 244 | 160.0, 245 | 28.0, 246 | Color::from_rgba(128, 16, 8, 255), 247 | ); 248 | 249 | draw_button( 250 | frame, 251 | &data.fonts, 252 | None, 253 | "Cancel", 254 | x + 170.0, 255 | y, 256 | 110.0, 257 | 28.0, 258 | Color::from_rgba(0, 0, 0, 0), 259 | ); 260 | 261 | draw_thumbnails(frame, &data.images, 365.0, popy - 30.0, 160.0, 300.0, t); 262 | } 263 | 264 | fn draw_eyes(frame: &Frame, x: f32, y: f32, w: f32, h: f32, mx: f32, my: f32, t: f32) { 265 | let ex = w * 0.23; 266 | let ey = h * 0.5; 267 | let lx = x + ex; 268 | let ly = y + ey; 269 | let rx = x + w - ex; 270 | let ry = y + ey; 271 | let br = (if ex < ey { ex } else { ey }) * 0.5; 272 | let blink = 1.0 - ((t * 0.5).sin()).powf(200.0) * 0.8; 273 | 274 | // eye shades 275 | frame.path( 276 | |path| { 277 | path.ellipse((lx + 3.0, ly + 16.0), ex, ey); 278 | path.ellipse((rx + 3.0, ry + 16.0), ex, ey); 279 | 280 | path.fill( 281 | Gradient::Linear { 282 | start: (x, y + h * 0.5), 283 | end: (x + w * 0.1, y + h), 284 | start_color: Color::from_rgba(0, 0, 0, 32), 285 | end_color: Color::from_rgba(0, 0, 0, 16), 286 | }, 287 | Default::default(), 288 | ); 289 | }, 290 | Default::default(), 291 | ); 292 | 293 | // eye whites 294 | frame.path( 295 | |path| { 296 | path.ellipse((lx, ly), ex, ey); 297 | path.ellipse((rx, ry), ex, ey); 298 | 299 | path.fill( 300 | Gradient::Linear { 301 | start: (x, y + h * 0.25), 302 | end: (x + w * 0.1, y + h), 303 | start_color: Color::from_rgba(220, 220, 220, 255), 304 | end_color: Color::from_rgba(128, 128, 128, 255), 305 | }, 306 | Default::default(), 307 | ); 308 | }, 309 | Default::default(), 310 | ); 311 | 312 | // eye pupils 313 | frame.path( 314 | |path| { 315 | let mut dx = (mx - rx) / (ex * 10.0); 316 | let mut dy = (my - ry) / (ey * 10.0); 317 | let d = (dx * dx + dy * dy).sqrt(); 318 | 319 | if d > 1.0 { 320 | dx /= d; 321 | dy /= d; 322 | } 323 | 324 | dx *= ex * 0.4; 325 | dy *= ey * 0.5; 326 | 327 | path.ellipse((lx + dx, ly + dy + ey * 0.25 * (1.0 - blink)), br, br * blink); 328 | path.ellipse((rx + dx, ry + dy + ey * 0.25 * (1.0 - blink)), br, br * blink); 329 | path.fill(Color::from_rgba(32, 32, 32, 255), Default::default()); 330 | }, 331 | Default::default(), 332 | ); 333 | 334 | // left eye gloss 335 | frame.path( 336 | |path| { 337 | path.ellipse((lx, ly), ex, ey); 338 | path.fill( 339 | Gradient::Radial { 340 | center: (lx - ex * 0.25, ly - ey * 0.5), 341 | inner_radius: ex * 0.1, 342 | outer_radius: ex * 0.75, 343 | start_color: Color::from_rgba(255, 255, 255, 128), 344 | end_color: Color::from_rgba(255, 255, 255, 0), 345 | }, 346 | Default::default(), 347 | ); 348 | }, 349 | Default::default(), 350 | ); 351 | 352 | // right eye gloss 353 | frame.path( 354 | |path| { 355 | path.ellipse((rx, ry), ex, ey); 356 | path.fill( 357 | Gradient::Radial { 358 | center: (rx - ex * 0.25, ry - ey * 0.5), 359 | inner_radius: ex * 0.1, 360 | outer_radius: ex * 0.75, 361 | start_color: Color::from_rgba(255, 255, 255, 128), 362 | end_color: Color::from_rgba(255, 255, 255, 0), 363 | }, 364 | Default::default(), 365 | ); 366 | }, 367 | Default::default(), 368 | ); 369 | } 370 | 371 | fn draw_paragraph(frame: &Frame, fonts: &DemoFonts, x: f32, y: f32, width: f32, _height: f32, mx: f32, my: f32) { 372 | let text = "This is longer chunk of text.\n \n Would have used lorem ipsum but she was busy jumping over the lazy dog with the fox and all the men who came to the aid of the party.🎉"; 373 | let text_options = TextOptions { 374 | color: Color::from_rgba(255, 255, 255, 255), 375 | size: 18.0, 376 | align: Alignment::new().left().top(), 377 | ..Default::default() 378 | }; 379 | let metrics = frame.text_metrics(fonts.sans, text_options); 380 | 381 | let mut gutter_line = 0; 382 | let mut gutter_x = 0.0f32; 383 | let mut gutter_y = 0.0f32; 384 | 385 | let mut y = y; 386 | let mut line_number = 0; 387 | for row in frame.text_break_lines(fonts.sans, text, width, text_options) { 388 | line_number += 1; 389 | let hit = mx > x && mx < (x + width) && my >= y && my < (y + metrics.line_height); 390 | 391 | // draw line background 392 | frame.path( 393 | |path| { 394 | path.rect((x, y), (row.width, metrics.line_height)); 395 | path.fill( 396 | Color::from_rgba(255, 255, 255, if hit { 64 } else { 16 }), 397 | Default::default(), 398 | ); 399 | }, 400 | Default::default(), 401 | ); 402 | 403 | // draw line text 404 | frame.text(fonts.sans, (x, y), row.text, text_options); 405 | 406 | if hit { 407 | let mut caretx = if mx < x + row.width / 2.0 { x } else { x + row.width }; 408 | let mut px = x; 409 | 410 | // calculate mouse caret position 411 | let mut glyph_positions = frame.text_glyph_positions((x, y), row.text).peekable(); 412 | while let Some(glyph) = glyph_positions.next() { 413 | let x0 = glyph.x; 414 | let x1 = if let Some(next) = glyph_positions.peek() { 415 | next.x 416 | } else { 417 | x + row.width 418 | }; 419 | let gx = x0 * 0.3 + x1 * 0.7; 420 | 421 | if mx >= px && mx < gx { 422 | caretx = x0; 423 | break; 424 | } 425 | 426 | px = gx; 427 | } 428 | 429 | // draw mouse caret 430 | frame.path( 431 | |path| { 432 | path.rect((caretx, y), (1.0, metrics.line_height)); 433 | path.fill(Color::from_rgba(255, 192, 0, 255), Default::default()); 434 | }, 435 | Default::default(), 436 | ); 437 | 438 | gutter_x = x - 10.0; 439 | gutter_y = y + metrics.line_height / 2.0; 440 | gutter_line = line_number; 441 | } 442 | 443 | y += metrics.line_height; 444 | } 445 | 446 | // draw gutter 447 | if gutter_line != 0 { 448 | let gx = gutter_x; 449 | let gy = gutter_y; 450 | let gutter_text = format!("{}", gutter_line); 451 | let gutter_text_options = TextOptions { 452 | color: Color::from_rgba(32, 32, 32, 255), 453 | size: 13.0, 454 | align: Alignment::new().right().middle(), 455 | ..Default::default() 456 | }; 457 | let (_, bounds) = frame.text_bounds(fonts.sans, (gx, gy), &gutter_text, gutter_text_options); 458 | frame.path( 459 | |path| { 460 | path.rounded_rect( 461 | (bounds.min_x - 4.0, bounds.min_y - 2.0), 462 | (bounds.max_x - bounds.min_x + 8.0, bounds.max_y - bounds.min_y + 4.0), 463 | (bounds.max_y - bounds.min_y + 4.0) / 2.0 - 1.0, 464 | ); 465 | 466 | path.fill(Color::from_rgba(255, 192, 0, 255), Default::default()); 467 | }, 468 | Default::default(), 469 | ); 470 | frame.text(fonts.sans, (gx, gy), &gutter_text, gutter_text_options); 471 | } 472 | 473 | y += 20.0; 474 | 475 | let tooltip_text = "Hover your mouse over the text to see calculated caret position."; 476 | let tooltip_opts = TextOptions { 477 | color: Color::from_rgba(0, 0, 0, 220), 478 | size: 13.0, 479 | align: Alignment::new().left().top(), 480 | line_height: 1.2, 481 | line_max_width: 150.0, 482 | ..Default::default() 483 | }; 484 | // draw tooltip 485 | let bounds = frame.text_box_bounds(fonts.sans, (x, y), tooltip_text, tooltip_opts); 486 | let gx = f32::abs((mx - (bounds.min_x + bounds.max_x) * 0.5) / (bounds.min_x - bounds.max_x)); 487 | let gy = f32::abs((my - (bounds.min_y + bounds.max_y) * 0.5) / (bounds.min_y - bounds.max_y)); 488 | let alpha = f32::max(gx, gy) - 0.5; 489 | let alpha = clamp(alpha, 0.0, 1.0); 490 | 491 | frame.path( 492 | |path| { 493 | path.rounded_rect( 494 | (bounds.min_x - 2.0, bounds.min_y - 2.0), 495 | (bounds.max_x - bounds.min_x + 4.0, bounds.max_y - bounds.min_y + 4.0), 496 | 3.0, 497 | ); 498 | let px = (bounds.max_x + bounds.min_x) / 2.0; 499 | let py = bounds.min_y; 500 | path.move_to((px, py - 10.0)); 501 | path.line_to((px + 7.0, py + 1.0)); 502 | path.line_to((px - 7.0, py + 1.0)); 503 | path.fill(Color::from_rgba(220, 220, 220, 255), Default::default()); 504 | }, 505 | PathOptions { 506 | alpha, 507 | ..Default::default() 508 | }, 509 | ); 510 | 511 | frame.text_box(fonts.sans, (x, y), tooltip_text, tooltip_opts); 512 | } 513 | 514 | fn draw_graph(frame: &Frame, x: f32, y: f32, w: f32, h: f32, t: f32) { 515 | let mut samples = [0.0f32; 6]; 516 | let mut sx = [0.0f32; 6]; 517 | let mut sy = [0.0f32; 6]; 518 | let dx = w / 5.0; 519 | 520 | samples[0] = (1.0 + f32::sin(t * 1.2345 + f32::cos(t * 0.33457) * 0.44)) * 0.5; 521 | samples[1] = (1.0 + f32::sin(t * 0.68363 + f32::cos(t * 1.3) * 1.55)) * 0.5; 522 | samples[2] = (1.0 + f32::sin(t * 1.1642 + f32::cos(t * 0.33457) * 1.24)) * 0.5; 523 | samples[3] = (1.0 + f32::sin(t * 0.56345 + f32::cos(t * 1.63) * 0.14)) * 0.5; 524 | samples[4] = (1.0 + f32::sin(t * 1.6245 + f32::cos(t * 0.254) * 0.3)) * 0.5; 525 | samples[5] = (1.0 + f32::sin(t * 0.345 + f32::cos(t * 0.03) * 0.6)) * 0.5; 526 | 527 | for i in 0..6 { 528 | sx[i] = x + i as f32 * dx; 529 | sy[i] = y + h * samples[i] * 0.8; 530 | } 531 | 532 | // graph background 533 | frame.path( 534 | |path| { 535 | path.move_to((sx[0], sy[0])); 536 | for i in 1..6 { 537 | path.cubic_bezier_to( 538 | (sx[i], sy[i]), 539 | (sx[i - 1] + dx * 0.5, sy[i - 1]), 540 | (sx[i] - dx * 0.5, sy[i]), 541 | ); 542 | } 543 | 544 | path.line_to((x + w, y + h)); 545 | path.line_to((x, y + h)); 546 | 547 | path.fill( 548 | Gradient::Linear { 549 | start: (x, y), 550 | end: (x, y + h), 551 | start_color: Color::from_rgba(0, 160, 192, 0), 552 | end_color: Color::from_rgba(0, 160, 192, 64), 553 | }, 554 | Default::default(), 555 | ); 556 | }, 557 | Default::default(), 558 | ); 559 | 560 | // graph line (darker) 561 | frame.path( 562 | |path| { 563 | path.move_to((sx[0], sy[0] + 2.0)); 564 | for i in 1..6 { 565 | path.cubic_bezier_to( 566 | (sx[i], sy[i] + 2.0), 567 | (sx[i - 1] + dx * 0.5, sy[i - 1] + 2.0), 568 | (sx[i] - dx * 0.5, sy[i] + 2.0), 569 | ); 570 | } 571 | 572 | path.stroke( 573 | Color::from_rgba(0, 0, 0, 32), 574 | StrokeOptions { 575 | width: 3.0, 576 | ..Default::default() 577 | }, 578 | ); 579 | }, 580 | Default::default(), 581 | ); 582 | 583 | // graph line (lighter) 584 | frame.path( 585 | |path| { 586 | path.move_to((sx[0], sy[0])); 587 | for i in 1..6 { 588 | path.cubic_bezier_to( 589 | (sx[i], sy[i]), 590 | (sx[i - 1] + dx * 0.5, sy[i - 1]), 591 | (sx[i] - dx * 0.5, sy[i]), 592 | ); 593 | } 594 | 595 | path.stroke( 596 | Color::from_rgba(0, 160, 192, 255), 597 | StrokeOptions { 598 | width: 3.0, 599 | ..Default::default() 600 | }, 601 | ); 602 | }, 603 | Default::default(), 604 | ); 605 | 606 | // graph sample points (background shades) 607 | for i in 0..6 { 608 | frame.path( 609 | |path| { 610 | path.rect((sx[i] - 10.0, sy[i] - 10.0 + 2.0), (20.0, 20.0)); 611 | path.fill( 612 | Gradient::Radial { 613 | center: (sx[i], sy[i] + 2.0), 614 | inner_radius: 3.0, 615 | outer_radius: 8.0, 616 | start_color: Color::from_rgba(0, 0, 0, 32), 617 | end_color: Color::from_rgba(0, 0, 0, 0), 618 | }, 619 | Default::default(), 620 | ); 621 | }, 622 | Default::default(), 623 | ); 624 | } 625 | 626 | // graph sample points (main dots) 627 | frame.path( 628 | |path| { 629 | for i in 0..6 { 630 | path.circle((sx[i], sy[i]), 4.0); 631 | } 632 | 633 | path.fill(Color::from_rgba(0, 160, 192, 255), Default::default()); 634 | }, 635 | Default::default(), 636 | ); 637 | 638 | // graph sample points (small white dots) 639 | frame.path( 640 | |path| { 641 | for i in 0..6 { 642 | path.circle((sx[i], sy[i]), 2.0); 643 | } 644 | 645 | path.fill(Color::from_rgba(220, 220, 220, 255), Default::default()); 646 | }, 647 | Default::default(), 648 | ); 649 | } 650 | 651 | fn draw_color_wheel(frame: &Frame, x: f32, y: f32, w: f32, h: f32, t: f32) { 652 | let cx = x + w * 0.5; 653 | let cy = y + h * 0.5; 654 | let r1 = if w < h { w } else { h } * 0.5 - 5.0; 655 | let r0 = r1 - 20.0; 656 | let aeps = 0.5 / r1; // half a pixel arc length in radians (2pi cancels out). 657 | let hue = f32::sin(t * 0.12); 658 | 659 | for i in 0..6 { 660 | let a0 = i as f32 / 6.0 * consts::PI * 2.0 - aeps; 661 | let a1 = (i as f32 + 1.0) / 6.0 * consts::PI * 2.0 + aeps; 662 | 663 | // draw color segment gradient 664 | frame.path( 665 | |path| { 666 | path.arc((cx, cy), r0, a0, a1, Winding::Direction(Direction::Clockwise)); 667 | path.arc((cx, cy), r1, a1, a0, Winding::Direction(Direction::CounterClockwise)); 668 | let ax = cx + f32::cos(a0) * (r0 + r1) * 0.5; 669 | let ay = cy + f32::sin(a0) * (r0 + r1) * 0.5; 670 | let bx = cx + f32::cos(a1) * (r0 + r1) * 0.5; 671 | let by = cy + f32::sin(a1) * (r0 + r1) * 0.5; 672 | path.fill( 673 | Gradient::Linear { 674 | start: (ax, ay), 675 | end: (bx, by), 676 | start_color: Color::from_hsla(a0 / (consts::PI * 2.0), 1.0, 0.55, 255), 677 | end_color: Color::from_hsla(a1 / (consts::PI * 2.0), 1.0, 0.55, 255), 678 | }, 679 | Default::default(), 680 | ); 681 | }, 682 | Default::default(), 683 | ); 684 | } 685 | 686 | // draw circle outlines 687 | frame.path( 688 | |path| { 689 | path.circle((cx, cy), r0 - 0.5); 690 | path.circle((cx, cy), r1 + 0.5); 691 | path.stroke( 692 | Color::from_rgba(0, 0, 0, 64), 693 | StrokeOptions { 694 | width: 1.0, 695 | ..Default::default() 696 | }, 697 | ); 698 | }, 699 | Default::default(), 700 | ); 701 | 702 | let transform = Transform::new().translate(cx, cy).rotate(hue * consts::PI * 2.0); 703 | 704 | // color selector 705 | frame.path( 706 | |path| { 707 | path.rect((r0 - 1.0, -3.0), (r1 - r0 + 2.0, 6.0)); 708 | path.stroke( 709 | Color::from_rgba(255, 255, 255, 192), 710 | StrokeOptions { 711 | width: 2.0, 712 | ..Default::default() 713 | }, 714 | ); 715 | }, 716 | PathOptions { 717 | transform: Some(transform), 718 | ..Default::default() 719 | }, 720 | ); 721 | 722 | // color marker inside selector 723 | frame.path( 724 | |path| { 725 | path.rect((r0 - 2.0 - 10.0, -4.0 - 10.0), (r1 - r0 + 4.0 + 20.0, 8.0 + 20.0)); 726 | path.move_to((0.0, 0.0)); 727 | path.rect((r0 - 2.0, -4.0), (r1 - r0 + 4.0, 8.0)); 728 | path.winding(Winding::Solidity(Solidity::Hole)); 729 | path.fill( 730 | Gradient::Box { 731 | position: (r0 - 3.0, -5.0), 732 | size: (r1 - r0 + 6.0, 10.0), 733 | radius: 2.0, 734 | feather: 4.0, 735 | start_color: Color::from_rgba(0, 0, 0, 128), 736 | end_color: Color::from_rgba(0, 0, 0, 0), 737 | }, 738 | Default::default(), 739 | ); 740 | }, 741 | PathOptions { 742 | transform: Some(transform), 743 | ..Default::default() 744 | }, 745 | ); 746 | 747 | let r = r0 - 6.0; 748 | 749 | // center triangle 750 | frame.path( 751 | |path| { 752 | let ax = f32::cos(120.0 / 180.0 * consts::PI) * r; 753 | let ay = f32::sin(120.0 / 180.0 * consts::PI) * r; 754 | let bx = f32::cos(-120.0 / 180.0 * consts::PI) * r; 755 | let by = f32::sin(-120.0 / 180.0 * consts::PI) * r; 756 | 757 | path.move_to((r, 0.0)); 758 | path.line_to((ax, ay)); 759 | path.line_to((bx, by)); 760 | path.close(); 761 | path.fill( 762 | Gradient::Linear { 763 | start: (r, 0.0), 764 | end: (ax, ay), 765 | start_color: Color::from_hsla(hue, 1.0, 0.5, 255), 766 | end_color: Color::from_rgba(255, 255, 255, 255), 767 | }, 768 | Default::default(), 769 | ); 770 | path.fill( 771 | Gradient::Linear { 772 | start: ((r + ax) * 0.5, ((0.0 + ay) * 0.5)), 773 | end: (bx, by), 774 | start_color: Color::from_rgba(0, 0, 0, 0), 775 | end_color: Color::from_rgba(0, 0, 0, 255), 776 | }, 777 | Default::default(), 778 | ); 779 | path.stroke( 780 | Color::from_rgba(0, 0, 0, 64), 781 | StrokeOptions { 782 | width: 2.0, 783 | ..Default::default() 784 | }, 785 | ); 786 | }, 787 | PathOptions { 788 | transform: Some(transform), 789 | ..Default::default() 790 | }, 791 | ); 792 | 793 | let ax = f32::cos(120.0 / 180.0 * consts::PI) * r * 0.3; 794 | let ay = f32::sin(120.0 / 180.0 * consts::PI) * r * 0.4; 795 | 796 | // select circle on triangle 797 | frame.path( 798 | |path| { 799 | path.circle((ax, ay), 5.0); 800 | path.stroke( 801 | Color::from_rgba(255, 255, 255, 192), 802 | StrokeOptions { 803 | width: 2.0, 804 | ..Default::default() 805 | }, 806 | ); 807 | }, 808 | PathOptions { 809 | transform: Some(transform), 810 | ..Default::default() 811 | }, 812 | ); 813 | 814 | // select circle outline 815 | frame.path( 816 | |path| { 817 | path.rect((ax - 20.0, ay - 20.0), (40.0, 40.0)); 818 | path.move_to((0.0, 0.0)); 819 | path.circle((ax, ay), 7.0); 820 | path.winding(Winding::Solidity(Solidity::Hole)); 821 | path.fill( 822 | Gradient::Radial { 823 | center: (ax, ay), 824 | inner_radius: 7.0, 825 | outer_radius: 9.0, 826 | start_color: Color::from_rgba(0, 0, 0, 64), 827 | end_color: Color::from_rgba(0, 0, 0, 0), 828 | }, 829 | Default::default(), 830 | ); 831 | }, 832 | PathOptions { 833 | transform: Some(transform), 834 | ..Default::default() 835 | }, 836 | ); 837 | } 838 | 839 | fn draw_lines(frame: &Frame, x: f32, y: f32, w: f32, _h: f32, t: f32) { 840 | let pad = 5.0; 841 | let s = w / 9.0 - pad * 2.0; 842 | let mut pts = [0.0f32; 4 * 2]; 843 | let caps = [LineCap::Butt, LineCap::Round, LineCap::Square]; 844 | let joins = [LineJoin::Miter, LineJoin::Round, LineJoin::Bevel]; 845 | 846 | pts[0] = -s * 0.25 + f32::cos(t * 0.3) * s * 0.5; 847 | pts[1] = f32::sin(t * 0.3) * s * 0.5; 848 | pts[2] = -s * 0.25; 849 | pts[3] = 0.0; 850 | pts[4] = s * 0.25; 851 | pts[5] = 0.0; 852 | pts[6] = s * 0.25 + f32::cos(-t * 0.3) * s * 0.5; 853 | pts[7] = f32::sin(-t * 0.3) * s * 0.5; 854 | 855 | for i in 0..caps.len() { 856 | for j in 0..joins.len() { 857 | let fx = x + s * 0.5 + (i as f32 * 3.0 + j as f32) / 9.0 * w + pad; 858 | let fy = y - s * 0.5 + pad; 859 | 860 | let cap = caps[i]; 861 | let join = joins[j]; 862 | 863 | frame.path( 864 | |path| { 865 | path.move_to((fx + pts[0], fy + pts[1])); 866 | path.line_to((fx + pts[2], fy + pts[3])); 867 | path.line_to((fx + pts[4], fy + pts[5])); 868 | path.line_to((fx + pts[6], fy + pts[7])); 869 | 870 | path.stroke( 871 | Color::from_rgba(0, 0, 0, 160), 872 | StrokeOptions { 873 | width: s * 0.3, 874 | line_cap: cap, 875 | line_join: join, 876 | ..Default::default() 877 | }, 878 | ); 879 | }, 880 | Default::default(), 881 | ); 882 | 883 | frame.path( 884 | |path| { 885 | path.move_to((fx + pts[0], fy + pts[1])); 886 | path.line_to((fx + pts[2], fy + pts[3])); 887 | path.line_to((fx + pts[4], fy + pts[5])); 888 | path.line_to((fx + pts[6], fy + pts[7])); 889 | 890 | path.stroke( 891 | Color::from_rgba(0, 192, 255, 255), 892 | StrokeOptions { 893 | width: 1.0, 894 | line_cap: cap, 895 | line_join: join, 896 | ..Default::default() 897 | }, 898 | ); 899 | }, 900 | Default::default(), 901 | ); 902 | } 903 | } 904 | } 905 | 906 | fn draw_widths(frame: &Frame, x: f32, y: f32, width: f32) { 907 | let mut y = y; 908 | 909 | for i in 0..20 { 910 | let w = (i as f32 + 0.5) * 0.1; 911 | frame.path( 912 | |path| { 913 | path.move_to((x, y)); 914 | path.line_to((x + width, y + width * 0.3)); 915 | 916 | path.stroke( 917 | Color::from_rgba(0, 0, 0, 255), 918 | StrokeOptions { 919 | width: w, 920 | ..Default::default() 921 | }, 922 | ); 923 | 924 | y += 10.0; 925 | }, 926 | Default::default(), 927 | ) 928 | } 929 | } 930 | 931 | fn draw_caps(frame: &Frame, x: f32, y: f32, width: f32) { 932 | let caps = [LineCap::Butt, LineCap::Round, LineCap::Square]; 933 | let line_width = 8.0; 934 | 935 | frame.path( 936 | |path| { 937 | path.rect((x - line_width / 2.0, y), (width + line_width, 40.0)); 938 | path.fill(Color::from_rgba(255, 255, 255, 32), Default::default()); 939 | }, 940 | Default::default(), 941 | ); 942 | 943 | frame.path( 944 | |path| { 945 | path.rect((x, y), (width, 40.0)); 946 | path.fill(Color::from_rgba(255, 255, 255, 32), Default::default()); 947 | }, 948 | Default::default(), 949 | ); 950 | 951 | for i in 0..caps.len() { 952 | frame.path( 953 | |path| { 954 | path.move_to((x, y + i as f32 * 10.0 + 5.0)); 955 | path.line_to((x + width, y + i as f32 * 10.0 + 5.0)); 956 | 957 | path.stroke( 958 | Color::from_rgba(0, 0, 0, 255), 959 | StrokeOptions { 960 | width: line_width, 961 | line_cap: caps[i], 962 | ..Default::default() 963 | }, 964 | ); 965 | }, 966 | Default::default(), 967 | ); 968 | } 969 | } 970 | 971 | fn draw_scissor(frame: &Frame, x: f32, y: f32, t: f32) { 972 | let first_transform = Transform::new().translate(x, y).rotate(5.0f32.to_radians()); 973 | 974 | frame.path( 975 | |path| { 976 | path.rect((-20.0, -20.0), (60.0, 40.0)); 977 | path.fill(Color::from_rgba(255, 0, 0, 255), Default::default()); 978 | }, 979 | PathOptions { 980 | transform: Some(first_transform), 981 | ..Default::default() 982 | }, 983 | ); 984 | 985 | let second_transform = first_transform.translate(40.0, 0.0).rotate(t); 986 | 987 | frame.path( 988 | |path| { 989 | path.rect((-20.0, -10.0), (60.0, 30.0)); 990 | path.fill(Color::from_rgba(255, 128, 0, 64), Default::default()); 991 | }, 992 | PathOptions { 993 | transform: Some(second_transform), 994 | ..Default::default() 995 | }, 996 | ); 997 | 998 | frame.path( 999 | |path| { 1000 | path.rect((-20.0, -10.0), (60.0, 30.0)); 1001 | path.fill(Color::from_rgba(255, 128, 0, 255), Default::default()); 1002 | }, 1003 | PathOptions { 1004 | clip: Clip::Intersect(Intersect { 1005 | x: -20.0, 1006 | y: -10.0, 1007 | width: 60.0, 1008 | height: 30.0, 1009 | with: Scissor { 1010 | x: -20.0, 1011 | y: -20.0, 1012 | width: 60.0, 1013 | height: 40.0, 1014 | transform: Some(first_transform), 1015 | }, 1016 | transform: Some(second_transform), 1017 | }), 1018 | transform: Some(second_transform), 1019 | ..Default::default() 1020 | }, 1021 | ); 1022 | } 1023 | 1024 | fn draw_window(frame: &Frame, fonts: &DemoFonts, title: &str, x: f32, y: f32, w: f32, h: f32) { 1025 | let corner_radius = 3.0; 1026 | 1027 | // window background 1028 | frame.path( 1029 | |path| { 1030 | path.rounded_rect((x, y), (w, h), corner_radius); 1031 | path.fill(Color::from_rgba(28, 30, 34, 192), Default::default()); 1032 | }, 1033 | Default::default(), 1034 | ); 1035 | 1036 | // drop shadow 1037 | frame.path( 1038 | |path| { 1039 | path.rect((x - 10.0, y - 10.0), (w + 20.0, h + 30.0)); 1040 | path.move_to((x, y)); 1041 | path.rounded_rect((x, y), (w, h), corner_radius); 1042 | path.winding(Winding::Solidity(Solidity::Hole)); 1043 | path.fill( 1044 | Gradient::Box { 1045 | position: (x, y + 2.0), 1046 | size: (w, h), 1047 | radius: corner_radius * 2.0, 1048 | feather: 10.0, 1049 | start_color: Color::from_rgba(0, 0, 0, 128), 1050 | end_color: Color::from_rgba(0, 0, 0, 0), 1051 | }, 1052 | Default::default(), 1053 | ); 1054 | }, 1055 | Default::default(), 1056 | ); 1057 | 1058 | // header 1059 | frame.path( 1060 | |path| { 1061 | path.rounded_rect((x + 1.0, y + 1.0), (w - 2.0, 30.0), corner_radius - 1.0); 1062 | path.fill( 1063 | Gradient::Linear { 1064 | start: (x, y), 1065 | end: (x, y + 15.0), 1066 | start_color: Color::from_rgba(255, 255, 255, 8), 1067 | end_color: Color::from_rgba(0, 0, 0, 16), 1068 | }, 1069 | Default::default(), 1070 | ); 1071 | }, 1072 | Default::default(), 1073 | ); 1074 | 1075 | // header separator 1076 | frame.path( 1077 | |path| { 1078 | path.move_to((x + 0.5, y + 0.5 + 30.0)); 1079 | path.line_to((x + 0.5 + w - 1.0, y + 0.5 + 30.0)); 1080 | 1081 | path.stroke(Color::from_rgba(0, 0, 0, 32), Default::default()); 1082 | }, 1083 | Default::default(), 1084 | ); 1085 | 1086 | // header text 1087 | frame.text( 1088 | fonts.sans_bold, 1089 | (x + w / 2.0, y + 16.0), 1090 | title, 1091 | TextOptions { 1092 | size: 18.0, 1093 | align: Alignment::new().center().middle(), 1094 | color: Color::from_rgba(0, 0, 0, 128), 1095 | blur: 2.0, 1096 | ..Default::default() 1097 | }, 1098 | ); 1099 | 1100 | frame.text( 1101 | fonts.sans_bold, 1102 | (x + w / 2.0, y + 16.0), 1103 | title, 1104 | TextOptions { 1105 | size: 18.0, 1106 | align: Alignment::new().center().middle(), 1107 | color: Color::from_rgba(220, 220, 220, 160), 1108 | blur: 0.0, 1109 | ..Default::default() 1110 | }, 1111 | ); 1112 | } 1113 | 1114 | fn draw_search_box(frame: &Frame, fonts: &DemoFonts, text: &str, x: f32, y: f32, w: f32, h: f32) { 1115 | let corner_radius = h / 2.0 - 1.0; 1116 | 1117 | // background rounded rectangle 1118 | frame.path( 1119 | |path| { 1120 | path.rounded_rect((x, y), (w, h), corner_radius); 1121 | path.fill( 1122 | Gradient::Box { 1123 | position: (x, y + 1.5), 1124 | size: (w, h), 1125 | radius: h / 2.0, 1126 | feather: 5.0, 1127 | start_color: Color::from_rgba(0, 0, 0, 16), 1128 | end_color: Color::from_rgba(0, 0, 0, 92), 1129 | }, 1130 | Default::default(), 1131 | ); 1132 | }, 1133 | Default::default(), 1134 | ); 1135 | 1136 | frame.text( 1137 | fonts.icons, 1138 | (x + h * 0.55, y + h * 0.55), 1139 | ICON_SEARCH, 1140 | TextOptions { 1141 | color: Color::from_rgba(255, 255, 255, 64), 1142 | size: h * 1.3, 1143 | align: Alignment::new().center().middle(), 1144 | ..Default::default() 1145 | }, 1146 | ); 1147 | 1148 | frame.text( 1149 | fonts.sans, 1150 | (x + h * 1.05, y + h * 0.5), 1151 | text, 1152 | TextOptions { 1153 | color: Color::from_rgba(255, 255, 255, 32), 1154 | size: 20.0, 1155 | align: Alignment::new().left().middle(), 1156 | ..Default::default() 1157 | }, 1158 | ); 1159 | 1160 | frame.text( 1161 | fonts.icons, 1162 | (x + w - h * 0.55, y + h * 0.55), 1163 | ICON_CIRCLED_CROSS, 1164 | TextOptions { 1165 | color: Color::from_rgba(255, 255, 255, 32), 1166 | size: h * 1.3, 1167 | align: Alignment::new().center().middle(), 1168 | ..Default::default() 1169 | }, 1170 | ); 1171 | } 1172 | 1173 | fn draw_drop_down(frame: &Frame, fonts: &DemoFonts, text: &str, x: f32, y: f32, w: f32, h: f32) { 1174 | let corner_radius = 4.0; 1175 | 1176 | // drop down button with linear gradient 1177 | frame.path( 1178 | |path| { 1179 | path.rounded_rect((x + 1.0, y + 1.0), (w - 2.0, h - 2.0), corner_radius - 1.0); 1180 | path.fill( 1181 | Gradient::Linear { 1182 | start: (x, y), 1183 | end: (x, y + h), 1184 | start_color: Color::from_rgba(255, 255, 255, 16), 1185 | end_color: Color::from_rgba(0, 0, 0, 16), 1186 | }, 1187 | Default::default(), 1188 | ); 1189 | }, 1190 | Default::default(), 1191 | ); 1192 | 1193 | // border arond drop down 1194 | frame.path( 1195 | |path| { 1196 | path.rounded_rect((x + 0.5, y + 0.5), (w - 1.0, h - 1.0), corner_radius - 0.5); 1197 | path.stroke(Color::from_rgba(0, 0, 0, 48), Default::default()); 1198 | }, 1199 | Default::default(), 1200 | ); 1201 | 1202 | // main drop down text 1203 | frame.text( 1204 | fonts.sans, 1205 | (x + h * 0.3, y + h * 0.5), 1206 | text, 1207 | TextOptions { 1208 | color: Color::from_rgba(255, 255, 255, 160), 1209 | size: 20.0, 1210 | align: Alignment::new().left().middle(), 1211 | ..Default::default() 1212 | }, 1213 | ); 1214 | 1215 | // chevron on right 1216 | frame.text( 1217 | fonts.icons, 1218 | (x + w - h * 0.5, y + h * 0.5), 1219 | ICON_CHEVRON_RIGHT, 1220 | TextOptions { 1221 | color: Color::from_rgba(255, 255, 255, 64), 1222 | size: h * 1.3, 1223 | align: Alignment::new().center().middle(), 1224 | ..Default::default() 1225 | }, 1226 | ); 1227 | } 1228 | 1229 | fn draw_label(frame: &Frame, fonts: &DemoFonts, text: &str, x: f32, y: f32, _w: f32, h: f32) { 1230 | frame.text( 1231 | fonts.sans, 1232 | (x, y + h * 0.5), 1233 | text, 1234 | TextOptions { 1235 | size: 18.0, 1236 | color: Color::from_rgba(255, 255, 255, 128), 1237 | align: Alignment::new().left().middle(), 1238 | ..Default::default() 1239 | }, 1240 | ); 1241 | } 1242 | 1243 | fn draw_edit_box_base(frame: &Frame, x: f32, y: f32, w: f32, h: f32) { 1244 | let corner_radius = 4.0; 1245 | 1246 | // base background 1247 | frame.path( 1248 | |path| { 1249 | path.rounded_rect((x + 1.0, y + 1.0), (w - 2.0, h - 2.0), corner_radius - 1.0); 1250 | path.fill( 1251 | Gradient::Box { 1252 | position: (x + 1.0, y + 1.0 + 1.5), 1253 | size: (w - 2.0, h - 2.0), 1254 | radius: 3.0, 1255 | feather: 4.0, 1256 | start_color: Color::from_rgba(255, 255, 255, 32), 1257 | end_color: Color::from_rgba(32, 32, 32, 32), 1258 | }, 1259 | Default::default(), 1260 | ); 1261 | }, 1262 | Default::default(), 1263 | ); 1264 | 1265 | // base border 1266 | frame.path( 1267 | |path| { 1268 | path.rounded_rect((x + 0.5, y + 0.5), (w - 1.0, h - 1.0), corner_radius - 0.5); 1269 | path.stroke(Color::from_rgba(0, 0, 0, 48), Default::default()); 1270 | }, 1271 | Default::default(), 1272 | ); 1273 | } 1274 | 1275 | fn draw_edit_box(frame: &Frame, fonts: &DemoFonts, text: &str, x: f32, y: f32, w: f32, h: f32) { 1276 | draw_edit_box_base(frame, x, y, w, h); 1277 | 1278 | frame.text( 1279 | fonts.sans, 1280 | (x + h * 0.3, y + h * 0.5), 1281 | text, 1282 | TextOptions { 1283 | size: 20.0, 1284 | color: Color::from_rgba(255, 255, 255, 64), 1285 | align: Alignment::new().left().middle(), 1286 | ..Default::default() 1287 | }, 1288 | ); 1289 | } 1290 | 1291 | fn draw_edit_box_num(frame: &Frame, fonts: &DemoFonts, text: &str, units: &str, x: f32, y: f32, w: f32, h: f32) { 1292 | draw_edit_box_base(frame, x, y, w, h); 1293 | 1294 | let units_options = TextOptions { 1295 | size: 18.0, 1296 | color: Color::from_rgba(255, 255, 255, 64), 1297 | align: Alignment::new().right().middle(), 1298 | ..Default::default() 1299 | }; 1300 | 1301 | let (uw, _) = frame.text_bounds(fonts.sans, (0.0, 0.0), units, units_options); 1302 | 1303 | frame.text(fonts.sans, (x + w - h * 0.3, y + h * 0.5), units, units_options); 1304 | 1305 | frame.text( 1306 | fonts.sans, 1307 | (x + w - uw - h * 0.5, y + h * 0.5), 1308 | text, 1309 | TextOptions { 1310 | size: 20.0, 1311 | color: Color::from_rgba(255, 255, 255, 128), 1312 | align: Alignment::new().right().middle(), 1313 | ..Default::default() 1314 | }, 1315 | ); 1316 | } 1317 | 1318 | fn draw_check_box(frame: &Frame, fonts: &DemoFonts, text: &str, x: f32, y: f32, _w: f32, h: f32) { 1319 | // checkbox text 1320 | frame.text( 1321 | fonts.sans, 1322 | (x + 28.0, y + h * 0.5), 1323 | text, 1324 | TextOptions { 1325 | size: 18.0, 1326 | color: Color::from_rgba(255, 255, 255, 160), 1327 | align: Alignment::new().left().middle(), 1328 | ..Default::default() 1329 | }, 1330 | ); 1331 | 1332 | // tick box 1333 | frame.path( 1334 | |path| { 1335 | path.rounded_rect((x + 1.0, y + h * 0.5 - 9.0), (18.0, 18.0), 3.0); 1336 | path.fill( 1337 | Gradient::Box { 1338 | position: (x + 1.0, y + h * 0.5 - 9.0 + 1.0), 1339 | size: (18.0, 18.0), 1340 | radius: 3.0, 1341 | feather: 3.0, 1342 | start_color: Color::from_rgba(0, 0, 0, 32), 1343 | end_color: Color::from_rgba(0, 0, 0, 92), 1344 | }, 1345 | Default::default(), 1346 | ); 1347 | }, 1348 | Default::default(), 1349 | ); 1350 | 1351 | // tick icon 1352 | frame.text( 1353 | fonts.icons, 1354 | (x + 9.0 + 2.0, y + h * 0.5), 1355 | ICON_CHECK, 1356 | TextOptions { 1357 | size: 40.0, 1358 | color: Color::from_rgba(255, 255, 255, 128), 1359 | align: Alignment::new().center().middle(), 1360 | ..Default::default() 1361 | }, 1362 | ); 1363 | } 1364 | 1365 | fn draw_button( 1366 | frame: &Frame, 1367 | fonts: &DemoFonts, 1368 | preicon: Option<&str>, 1369 | text: &str, 1370 | x: f32, 1371 | y: f32, 1372 | w: f32, 1373 | h: f32, 1374 | color: Color, 1375 | ) { 1376 | let corner_radius = 4.0; 1377 | let color_is_black = is_black(color); 1378 | 1379 | // button background 1380 | frame.path( 1381 | |path| { 1382 | path.rounded_rect((x + 1.0, y + 1.0), (w - 2.0, h - 2.0), corner_radius - 0.5); 1383 | if !color_is_black { 1384 | path.fill(color, Default::default()); 1385 | } 1386 | 1387 | path.fill( 1388 | Gradient::Linear { 1389 | start: (x, y), 1390 | end: (x, y + h), 1391 | start_color: Color::from_rgba(255, 255, 255, if color_is_black { 16 } else { 32 }), 1392 | end_color: Color::from_rgba(0, 0, 0, if color_is_black { 16 } else { 32 }), 1393 | }, 1394 | Default::default(), 1395 | ); 1396 | }, 1397 | Default::default(), 1398 | ); 1399 | 1400 | // button border 1401 | frame.path( 1402 | |path| { 1403 | path.rounded_rect((x + 0.5, y + 0.5), (w - 1.0, h - 1.0), corner_radius - 0.5); 1404 | path.stroke(Color::from_rgba(0, 0, 0, 48), Default::default()); 1405 | }, 1406 | Default::default(), 1407 | ); 1408 | 1409 | let (tw, _) = frame.text_bounds( 1410 | fonts.sans_bold, 1411 | (0.0, 0.0), 1412 | text, 1413 | TextOptions { 1414 | size: 20.0, 1415 | ..Default::default() 1416 | }, 1417 | ); 1418 | 1419 | let mut iw = 0.0; 1420 | 1421 | if let Some(icon) = preicon { 1422 | let icon_options = TextOptions { 1423 | size: h * 1.3, 1424 | color: Color::from_rgba(255, 255, 255, 96), 1425 | align: Alignment::new().left().middle(), 1426 | ..Default::default() 1427 | }; 1428 | 1429 | iw = frame.text_bounds(fonts.icons, (0.0, 0.0), icon, icon_options).0; 1430 | iw += h * 0.15; 1431 | 1432 | frame.text( 1433 | fonts.icons, 1434 | (x + w * 0.5 - tw * 0.5 - iw * 0.75, y + h * 0.5), 1435 | icon, 1436 | icon_options, 1437 | ); 1438 | } 1439 | 1440 | let mut options = TextOptions { 1441 | size: 20.0, 1442 | align: Alignment::new().left().middle(), 1443 | ..Default::default() 1444 | }; 1445 | 1446 | options.color = Color::from_rgba(0, 0, 0, 160); 1447 | 1448 | frame.text( 1449 | fonts.sans_bold, 1450 | (x + w * 0.5 - tw * 0.5 + iw * 0.25, y + h * 0.5 - 1.0), 1451 | text, 1452 | options, 1453 | ); 1454 | 1455 | options.color = Color::from_rgba(255, 255, 255, 160); 1456 | 1457 | frame.text( 1458 | fonts.sans_bold, 1459 | (x + w * 0.5 - tw * 0.5 + iw * 0.25, y + h * 0.5), 1460 | text, 1461 | options, 1462 | ); 1463 | } 1464 | 1465 | fn draw_slider(frame: &Frame, value: f32, x: f32, y: f32, w: f32, h: f32) { 1466 | let cy = y + h * 0.5; 1467 | let kr = h * 0.25; 1468 | let corner_radius = 2.0; 1469 | 1470 | // slot bar 1471 | frame.path( 1472 | |path| { 1473 | path.rounded_rect((x, cy - 2.0), (w, 4.0), corner_radius); 1474 | path.fill( 1475 | Gradient::Box { 1476 | position: (x, cy - 2.0 + 1.0), 1477 | size: (w, 4.0), 1478 | radius: 2.0, 1479 | feather: 2.0, 1480 | start_color: Color::from_rgba(0, 0, 0, 32), 1481 | end_color: Color::from_rgba(0, 0, 0, 128), 1482 | }, 1483 | Default::default(), 1484 | ); 1485 | }, 1486 | Default::default(), 1487 | ); 1488 | 1489 | // knob shadow 1490 | frame.path( 1491 | |path| { 1492 | path.rect( 1493 | (x + (value * w) - kr - 5.0, cy - kr - 5.0), 1494 | (kr * 2.0 + 5.0 + 5.0, kr * 2.0 + 5.0 + 5.0 + 3.0), 1495 | ); 1496 | path.move_to((x, y)); 1497 | path.circle((x + value * w, cy), kr); 1498 | path.winding(Winding::Solidity(Solidity::Hole)); 1499 | 1500 | path.fill( 1501 | Gradient::Radial { 1502 | center: (x + value * w, cy + 1.0), 1503 | inner_radius: kr - 3.0, 1504 | outer_radius: kr + 3.0, 1505 | start_color: Color::from_rgba(0, 0, 0, 64), 1506 | end_color: Color::from_rgba(0, 0, 0, 0), 1507 | }, 1508 | Default::default(), 1509 | ); 1510 | }, 1511 | Default::default(), 1512 | ); 1513 | 1514 | // knob 1515 | frame.path( 1516 | |path| { 1517 | path.circle((x + (value * w), cy), kr - 1.0); 1518 | path.fill(Color::from_rgba(40, 43, 48, 255), Default::default()); 1519 | 1520 | path.fill( 1521 | Gradient::Linear { 1522 | start: (x, cy - kr), 1523 | end: (x, cy + kr), 1524 | start_color: Color::from_rgba(255, 255, 255, 16), 1525 | end_color: Color::from_rgba(0, 0, 0, 16), 1526 | }, 1527 | Default::default(), 1528 | ); 1529 | }, 1530 | Default::default(), 1531 | ); 1532 | 1533 | // knob outline 1534 | frame.path( 1535 | |path| { 1536 | path.circle((x + value * w, cy), kr - 0.5); 1537 | path.stroke(Color::from_rgba(0, 0, 0, 92), Default::default()); 1538 | }, 1539 | Default::default(), 1540 | ); 1541 | } 1542 | 1543 | fn draw_thumbnails(frame: &Frame, images: &Vec, x: f32, y: f32, w: f32, h: f32, t: f32) { 1544 | let corner_radius = 3.0; 1545 | let thumb = 60.0; 1546 | let stackh = (images.len() / 2) as f32 * (thumb + 10.0) + 10.0; 1547 | 1548 | frame.path( 1549 | |path| { 1550 | path.rect((x - 10.0, y - 10.0), (w + 20.0, h + 30.0)); 1551 | path.move_to((x, y)); 1552 | path.rounded_rect((x, y), (w, h), corner_radius); 1553 | path.winding(Winding::Solidity(Solidity::Hole)); 1554 | path.fill( 1555 | Gradient::Box { 1556 | position: (x, y + 4.0), 1557 | size: (w, h), 1558 | radius: corner_radius * 2.0, 1559 | feather: 20.0, 1560 | start_color: Color::from_rgba(0, 0, 0, 128), 1561 | end_color: Color::from_rgba(0, 0, 0, 0), 1562 | }, 1563 | Default::default(), 1564 | ); 1565 | }, 1566 | Default::default(), 1567 | ); 1568 | 1569 | // left arrow 1570 | frame.path( 1571 | |path| { 1572 | let arry = 30.5; 1573 | path.rounded_rect((x, y), (w, h), corner_radius); 1574 | path.move_to((x - 10.0, y + arry)); 1575 | path.line_to((x + 1.0, y + arry - 11.0)); 1576 | path.line_to((x + 1.0, y + arry + 11.0)); 1577 | path.fill(Color::from_rgba(200, 200, 200, 255), Default::default()); 1578 | }, 1579 | Default::default(), 1580 | ); 1581 | 1582 | let dv = 1.0 / (images.len() - 1) as f32; 1583 | let u = (1.0 + f32::cos(t * 0.5)) * 0.5; 1584 | let u2 = (1.0 - f32::cos(t * 0.2)) * 0.5; 1585 | for (i, image) in images.iter().enumerate() { 1586 | let tx = x + 10.0 + (i % 2) as f32 * (thumb + 10.0); 1587 | let ty = y + 10.0 + (i / 2) as f32 * (thumb + 10.0); 1588 | let (imgw, imgh) = image.size(); 1589 | let iw; 1590 | let ih; 1591 | let ix; 1592 | let iy; 1593 | 1594 | if imgw < imgh { 1595 | iw = thumb; 1596 | ih = iw * imgh as f32 / imgw as f32; 1597 | ix = 0.0; 1598 | iy = -(ih - thumb) * 0.5; 1599 | } else { 1600 | ih = thumb; 1601 | iw = ih * imgw as f32 / imgh as f32; 1602 | ix = -(iw - thumb) * 0.5; 1603 | iy = 0.0; 1604 | } 1605 | 1606 | let v = i as f32 * dv; 1607 | let a = clamp((u2 - v) / dv, 0.0, 1.0); 1608 | 1609 | let path_opts = PathOptions { 1610 | clip: Clip::Scissor(Scissor { 1611 | x, 1612 | y, 1613 | width: w, 1614 | height: h, 1615 | transform: None, 1616 | }), 1617 | transform: Some(Transform::new().translate(0.0, -(stackh - h) * u)), 1618 | ..Default::default() 1619 | }; 1620 | 1621 | if a < 1.0 { 1622 | draw_spinner(frame, path_opts, tx + thumb / 2.0, ty + thumb / 2.0, thumb * 0.25, t); 1623 | } 1624 | 1625 | // draw image 1626 | frame.path( 1627 | |path| { 1628 | path.rounded_rect((tx, ty), (thumb, thumb), 5.0); 1629 | path.fill( 1630 | ImagePattern { 1631 | image: image, 1632 | origin: (tx + ix, ty + iy), 1633 | size: (iw, ih), 1634 | angle: 0.0 / 180.0 * consts::PI, 1635 | alpha: a, 1636 | }, 1637 | Default::default(), 1638 | ); 1639 | }, 1640 | path_opts, 1641 | ); 1642 | 1643 | // draw image background shade 1644 | frame.path( 1645 | |path| { 1646 | path.rect((tx - 5.0, ty - 5.0), (thumb + 10.0, thumb + 10.0)); 1647 | path.move_to((tx, ty)); 1648 | path.rounded_rect((tx, ty), (thumb, thumb), 6.0); 1649 | path.winding(Winding::Solidity(Solidity::Hole)); 1650 | path.fill( 1651 | Gradient::Box { 1652 | position: (tx - 1.0, ty), 1653 | size: (thumb + 2.0, thumb + 2.0), 1654 | radius: 5.0, 1655 | feather: 3.0, 1656 | start_color: Color::from_rgba(0, 0, 0, 128), 1657 | end_color: Color::from_rgba(0, 0, 0, 0), 1658 | }, 1659 | Default::default(), 1660 | ); 1661 | }, 1662 | path_opts, 1663 | ); 1664 | 1665 | // draw image border 1666 | frame.path( 1667 | |path| { 1668 | path.rounded_rect((tx + 0.5, ty + 0.5), (thumb - 1.0, thumb - 1.0), 4.0 - 0.5); 1669 | path.stroke( 1670 | Color::from_rgba(255, 255, 255, 192), 1671 | StrokeOptions { 1672 | width: 1.0, 1673 | ..Default::default() 1674 | }, 1675 | ); 1676 | }, 1677 | path_opts, 1678 | ); 1679 | } 1680 | 1681 | // white fade border (top) 1682 | frame.path( 1683 | |path| { 1684 | path.rect((x + 4.0, y), (w - 8.0, 6.0)); 1685 | path.fill( 1686 | Gradient::Linear { 1687 | start: (x, y), 1688 | end: (x, y + 6.0), 1689 | start_color: Color::from_rgba(200, 200, 200, 255), 1690 | end_color: Color::from_rgba(200, 200, 200, 0), 1691 | }, 1692 | Default::default(), 1693 | ); 1694 | }, 1695 | Default::default(), 1696 | ); 1697 | 1698 | // white fade border (bottom) 1699 | frame.path( 1700 | |path| { 1701 | path.rect((x + 4.0, y + h - 6.0), (w - 8.0, 6.0)); 1702 | path.fill( 1703 | Gradient::Linear { 1704 | start: (x, y + h), 1705 | end: (x, y + h - 6.0), 1706 | start_color: Color::from_rgba(200, 200, 200, 255), 1707 | end_color: Color::from_rgba(200, 200, 200, 0), 1708 | }, 1709 | Default::default(), 1710 | ); 1711 | }, 1712 | Default::default(), 1713 | ); 1714 | 1715 | // scrollbar socket 1716 | frame.path( 1717 | |path| { 1718 | path.rounded_rect((x + w - 12.0, y + 4.0), (8.0, h - 8.0), 3.0); 1719 | path.fill( 1720 | Gradient::Box { 1721 | position: (x + w - 12.0 + 1.0, y + 4.0 + 1.0), 1722 | size: (8.0, h - 8.0), 1723 | radius: 3.0, 1724 | feather: 4.0, 1725 | start_color: Color::from_rgba(0, 0, 0, 32), 1726 | end_color: Color::from_rgba(0, 0, 0, 92), 1727 | }, 1728 | Default::default(), 1729 | ); 1730 | }, 1731 | Default::default(), 1732 | ); 1733 | 1734 | let scrollh = (h / stackh) * (h - 8.0); 1735 | // scrollbar thumb 1736 | frame.path( 1737 | |path| { 1738 | path.rounded_rect( 1739 | (x + w - 12.0 + 1.0, y + 4.0 + 1.0 + (h - 8.0 - scrollh) * u), 1740 | (8.0 - 2.0, scrollh - 2.0), 1741 | 2.0, 1742 | ); 1743 | path.fill( 1744 | Gradient::Box { 1745 | position: (x + w - 12.0 - 1.0, y + 4.0 + (h - 8.0 - scrollh) * u - 1.0), 1746 | size: (8.0, scrollh), 1747 | radius: 3.0, 1748 | feather: 4.0, 1749 | start_color: Color::from_rgba(220, 220, 220, 255), 1750 | end_color: Color::from_rgba(128, 128, 128, 255), 1751 | }, 1752 | Default::default(), 1753 | ); 1754 | }, 1755 | Default::default(), 1756 | ); 1757 | } 1758 | 1759 | fn draw_spinner(frame: &Frame, options: PathOptions, cx: f32, cy: f32, r: f32, t: f32) { 1760 | let a0 = 0.0 + t * 6.0; 1761 | let a1 = consts::PI + t * 6.0; 1762 | let r0 = r; 1763 | let r1 = r * 0.75; 1764 | 1765 | frame.path( 1766 | |path| { 1767 | let ax = cx + f32::cos(a0) * (r0 + r1) * 0.5; 1768 | let ay = cy + f32::sin(a0) * (r0 + r1) * 0.5; 1769 | let bx = cx + f32::cos(a1) * (r0 + r1) * 0.5; 1770 | let by = cy + f32::sin(a1) * (r0 + r1) * 0.5; 1771 | path.arc((cx, cy), r0, a0, a1, Winding::Direction(Direction::Clockwise)); 1772 | path.arc((cx, cy), r1, a1, a0, Winding::Direction(Direction::CounterClockwise)); 1773 | path.fill( 1774 | Gradient::Linear { 1775 | start: (ax, ay), 1776 | end: (bx, by), 1777 | start_color: Color::from_rgba(0, 0, 0, 0), 1778 | end_color: Color::from_rgba(0, 0, 0, 128), 1779 | }, 1780 | Default::default(), 1781 | ); 1782 | }, 1783 | options, 1784 | ); 1785 | } 1786 | 1787 | enum GraphRenderStyle { 1788 | Fps, 1789 | Ms, 1790 | Percent, 1791 | } 1792 | 1793 | struct PerformanceGraph { 1794 | style: GraphRenderStyle, 1795 | name: String, 1796 | values: [f32; GRAPH_HISTORY_COUNT], 1797 | head: usize, 1798 | } 1799 | 1800 | impl PerformanceGraph { 1801 | fn new(style: GraphRenderStyle, name: &str) -> PerformanceGraph { 1802 | PerformanceGraph { 1803 | style, 1804 | name: String::from(name), 1805 | values: [0.0; GRAPH_HISTORY_COUNT], 1806 | head: 0, 1807 | } 1808 | } 1809 | 1810 | fn update(&mut self, frame_time: f32) { 1811 | self.head = (self.head + 1) % GRAPH_HISTORY_COUNT; 1812 | self.values[self.head] = frame_time; 1813 | } 1814 | 1815 | fn draw(&self, frame: &Frame, font: Font, x: f32, y: f32) { 1816 | let w = 200.0; 1817 | let h = 35.0; 1818 | let average = self.average(); 1819 | 1820 | frame.path( 1821 | |path| { 1822 | path.rect((x, y), (w, h)); 1823 | path.fill(Color::from_rgba(0, 0, 0, 128), Default::default()); 1824 | }, 1825 | Default::default(), 1826 | ); 1827 | 1828 | frame.path( 1829 | |path| { 1830 | path.move_to((x, y + h)); 1831 | match self.style { 1832 | GraphRenderStyle::Fps => { 1833 | for i in 0..self.values.len() { 1834 | let v = 1.0 / (0.00001 + self.values[(self.head + i) % self.values.len()]); 1835 | let v = clamp(v, 0.0, 80.0); 1836 | let vx = x + (i as f32 / (self.values.len() - 1) as f32) * w; 1837 | let vy = y + h - ((v / 80.0) * h); 1838 | path.line_to((vx, vy)); 1839 | } 1840 | } 1841 | GraphRenderStyle::Ms => { 1842 | for i in 0..self.values.len() { 1843 | let v = self.values[(self.head + i) % self.values.len()] * 1000.0; 1844 | let v = clamp(v, 0.0, 20.0); 1845 | let vx = x + (i as f32 / (self.values.len() - 1) as f32) * w; 1846 | let vy = y + h - ((v / 20.0) * h); 1847 | path.line_to((vx, vy)); 1848 | } 1849 | } 1850 | GraphRenderStyle::Percent => { 1851 | for i in 0..self.values.len() { 1852 | let v = self.values[(self.head + i) % self.values.len()] * 1.0; 1853 | let v = clamp(v, 0.0, 100.0); 1854 | let vx = x + (i as f32 / (self.values.len() - 1) as f32) * w; 1855 | let vy = y + h - ((v / 100.0) * h); 1856 | path.line_to((vx, vy)); 1857 | } 1858 | } 1859 | } 1860 | 1861 | path.line_to((x + w, y + h)); 1862 | 1863 | path.fill(Color::from_rgba(255, 192, 0, 128), Default::default()); 1864 | }, 1865 | Default::default(), 1866 | ); 1867 | 1868 | frame.text( 1869 | font, 1870 | (x + 3.0, y + 1.0), 1871 | &self.name, 1872 | TextOptions { 1873 | color: Color::from_rgba(240, 240, 240, 192), 1874 | align: Alignment::new().left().top(), 1875 | size: 14.0, 1876 | ..Default::default() 1877 | }, 1878 | ); 1879 | 1880 | match self.style { 1881 | GraphRenderStyle::Fps => { 1882 | frame.text( 1883 | font, 1884 | (x + w - 3.0, y + 1.0), 1885 | format!("{:.2} FPS", 1.0 / average), 1886 | TextOptions { 1887 | size: 18.0, 1888 | color: Color::from_rgba(240, 240, 240, 255), 1889 | align: Alignment::new().right().top(), 1890 | ..Default::default() 1891 | }, 1892 | ); 1893 | 1894 | frame.text( 1895 | font, 1896 | (x + w - 3.0, y + h - 1.0), 1897 | format!("{:.2} ms", average * 1000.0), 1898 | TextOptions { 1899 | size: 15.0, 1900 | color: Color::from_rgba(240, 240, 240, 160), 1901 | align: Alignment::new().right().bottom(), 1902 | ..Default::default() 1903 | }, 1904 | ); 1905 | } 1906 | GraphRenderStyle::Ms => { 1907 | frame.text( 1908 | font, 1909 | (x + w - 3.0, y + 1.0), 1910 | format!("{:.2} ms", average * 1000.0), 1911 | TextOptions { 1912 | size: 18.0, 1913 | color: Color::from_rgba(240, 240, 240, 255), 1914 | align: Alignment::new().right().top(), 1915 | ..Default::default() 1916 | }, 1917 | ); 1918 | } 1919 | GraphRenderStyle::Percent => frame.text( 1920 | font, 1921 | (x + w - 3.0, y + 1.0), 1922 | format!("{:.1} %", average * 1.0), 1923 | TextOptions { 1924 | size: 18.0, 1925 | color: Color::from_rgba(240, 240, 240, 255), 1926 | align: Alignment::new().right().top(), 1927 | ..Default::default() 1928 | }, 1929 | ), 1930 | } 1931 | } 1932 | 1933 | fn average(&self) -> f32 { 1934 | self.values.iter().sum::() / self.values.len() as f32 1935 | } 1936 | } 1937 | -------------------------------------------------------------------------------- /nanovg-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nanovg-sys" 3 | version = "1.0.2" 4 | authors = ["Kevin Kelley "] 5 | build = "build.rs" 6 | links = "nanovg" 7 | description = "Native bindings to the NanoVG library" 8 | homepage = "https://github.com/KevinKelley/nanovg-rs" 9 | repository = "https://github.com/KevinKelley/nanovg-rs" 10 | keywords = ["nanovg", "bindings", "vector", "graphics", "opengl"] 11 | categories = ["rendering::graphics-api", "games", "gui"] 12 | license = "MIT/Zlib" 13 | 14 | [package.metadata.docs.rs] 15 | features = ["gl3"] 16 | 17 | [lib] 18 | name = "nanovg_sys" 19 | path = "lib.rs" 20 | 21 | [features] 22 | gl2 = [] 23 | gl3 = [] 24 | gles2 = [] 25 | gles3 = [] 26 | 27 | [dependencies] 28 | bitflags = "1.0.3" 29 | 30 | [build-dependencies] 31 | cc = "1.0" 32 | -------------------------------------------------------------------------------- /nanovg-sys/build.rs: -------------------------------------------------------------------------------- 1 | extern crate cc; 2 | 3 | use std::env; 4 | use std::path::Path; 5 | use std::process::Command; 6 | 7 | fn build_library(backend_macro: &str) { 8 | let target = env::var("TARGET").unwrap(); 9 | let mut config = cc::Build::new(); 10 | 11 | // Hide the nanovg warnings. Not really relevant to us. 12 | // cc::Build::warnings(false); // this does not disable warnings, it can be used only for enabling them 13 | config.flag_if_supported("-w"); // disable warnings for msvc and gcc 14 | // (msvc accepts /w or -w, gcc and clang only -w) 15 | 16 | config.include("nanovg/src"); 17 | config.file("nanovg/src/nanovg.c"); 18 | config.file("nanovg_shim.c"); 19 | config.define(backend_macro, None); 20 | if target.contains("linux") { 21 | println!("cargo:rustc-link-lib=GL"); 22 | } else if target.contains("darwin") { 23 | println!("cargo:rustc-link-lib=framework=OpenGL"); 24 | } else if target.contains("windows") { 25 | config.file("glad/glad.c"); 26 | config.include("glad"); 27 | } 28 | 29 | config.compile("nanovg"); 30 | } 31 | 32 | fn main() { 33 | let backend_macro = ["GL3", "GL2", "GLES3", "GLES2"] 34 | .iter() 35 | .filter(|f| env::var(format!("CARGO_FEATURE_{}", f)).is_ok()) 36 | .map(|f| format!("NANOVG_{}_IMPLEMENTATION", f)) 37 | .next() 38 | .expect( 39 | "Unable to determine the backend / implementation. Have you enabled one of the features?", 40 | ); 41 | 42 | // Initialize nanovg submodule if user forgot to clone parent repository with --recursive. 43 | if !Path::new("nanovg/.git").exists() { 44 | let _ = Command::new("git").args(&["submodule", "update", "--init"]) 45 | .status(); 46 | } 47 | 48 | build_library(&backend_macro); 49 | } 50 | -------------------------------------------------------------------------------- /nanovg-sys/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | #![allow(non_snake_case)] 3 | 4 | #[macro_use] 5 | extern crate bitflags; 6 | 7 | use std::os::raw::{c_char, c_uchar, c_int, c_float, c_void}; 8 | 9 | pub const FONS_INVALID: c_int = -1; 10 | 11 | pub type NVGcontext = c_void; 12 | 13 | // No reason to use a union here, since the nanovg guys 14 | // only used it for convenience. 15 | #[repr(C)] 16 | #[derive(Copy, Clone, Debug, PartialEq)] 17 | pub struct NVGcolor { 18 | pub rgba: [c_float; 4], 19 | } 20 | 21 | #[repr(C)] 22 | #[derive(Copy, Clone, Debug)] 23 | pub struct NVGpaint { 24 | pub xform: [c_float; 6], 25 | pub extent: [c_float; 2], 26 | pub radius: c_float, 27 | pub feather: c_float, 28 | pub innerColor: NVGcolor, 29 | pub outerColor: NVGcolor, 30 | pub image: c_int, 31 | } 32 | 33 | bitflags! { 34 | pub struct NVGwinding: c_int { 35 | const NVG_CCW = 1; 36 | const NVG_CW = 2; 37 | } 38 | } 39 | 40 | bitflags! { 41 | pub struct NVGsolidity: c_int { 42 | const NVG_SOLID = 1; 43 | const NVG_HOLE = 2; 44 | } 45 | } 46 | 47 | bitflags! { 48 | pub struct NVGalign: c_int { 49 | const NVG_ALIGN_LEFT = 1 << 0; 50 | const NVG_ALIGN_CENTER = 1 << 1; 51 | const NVG_ALIGN_RIGHT = 1 << 2; 52 | const NVG_ALIGN_TOP = 1 << 3; 53 | const NVG_ALIGN_MIDDLE = 1 << 4; 54 | const NVG_ALIGN_BOTTOM = 1 << 5; 55 | const NVG_ALIGN_BASELINE = 1 << 6; 56 | } 57 | } 58 | 59 | bitflags! { 60 | pub struct NVGblendFactor: c_int { 61 | const NVG_ZERO = 1<<0; 62 | const NVG_ONE = 1<<1; 63 | const NVG_SRC_COLOR = 1<<2; 64 | const NVG_ONE_MINUS_SRC_COLOR = 1<<3; 65 | const NVG_DST_COLOR = 1<<4; 66 | const NVG_ONE_MINUS_DST_COLOR = 1<<5; 67 | const NVG_SRC_ALPHA = 1<<6; 68 | const NVG_ONE_MINUS_SRC_ALPHA = 1<<7; 69 | const NVG_DST_ALPHA = 1<<8; 70 | const NVG_ONE_MINUS_DST_ALPHA = 1<<9; 71 | const NVG_SRC_ALPHA_SATURATE = 1<<10; 72 | } 73 | } 74 | 75 | bitflags! { 76 | pub struct NVGimageFlags: c_int { 77 | const NVG_IMAGE_GENERATE_MIPMAPS = 1 << 0; 78 | const NVG_IMAGE_REPEATX = 1 << 1; 79 | const NVG_IMAGE_REPEATY = 1 << 2; 80 | const NVG_IMAGE_FLIPY = 1 << 3; 81 | const NVG_IMAGE_PREMULTIPLIED = 1 << 4; 82 | const NVG_IMAGE_NEAREST = 1 << 5; 83 | } 84 | } 85 | 86 | bitflags! { 87 | // #[cfg(any(feature = "gl2", feature = "gl3", feature = "gles2", feature = "gles3"))] 88 | pub struct NVGcreateFlags: c_int { 89 | const NVG_ANTIALIAS = 1 << 0; 90 | const NVG_STENCIL_STROKES = 1 << 1; 91 | const NVG_DEBUG = 1 << 2; 92 | } 93 | } 94 | 95 | #[repr(C)] 96 | #[derive(Debug)] 97 | pub enum NVGlineCap { 98 | NVG_BUTT, 99 | NVG_ROUND, 100 | NVG_SQUARE, 101 | NVG_BEVEL, 102 | NVG_MITER, 103 | } 104 | 105 | #[repr(C)] 106 | #[derive(Debug)] 107 | pub enum NVGcompositeOperation { 108 | NVG_SOURCE_OVER, 109 | NVG_SOURCE_IN, 110 | NVG_SOURCE_OUT, 111 | NVG_ATOP, 112 | NVG_DESTINATION_OVER, 113 | NVG_DESTINATION_IN, 114 | NVG_DESTINATION_OUT, 115 | NVG_DESTINATION_ATOP, 116 | NVG_LIGHTER, 117 | NVG_COPY, 118 | NVG_XOR, 119 | } 120 | 121 | #[repr(C)] 122 | #[derive(Debug)] 123 | pub struct NVGcompositeOperationState { 124 | pub srcRGB: c_int, 125 | pub dstRGB: c_int, 126 | pub srcAlpha: c_int, 127 | pub dstAlpha: c_int, 128 | } 129 | 130 | #[repr(C)] 131 | #[derive(Copy, Clone, Debug)] 132 | pub struct NVGglyphPosition { 133 | pub s: *const c_char, 134 | pub x: c_float, 135 | pub minx: c_float, 136 | pub maxx: c_float, 137 | } 138 | 139 | #[repr(C)] 140 | #[derive(Debug)] 141 | pub struct NVGtextRow { 142 | pub start: *const c_char, 143 | pub end: *const c_char, 144 | pub next: *const c_char, 145 | pub width: c_float, 146 | pub minx: c_float, 147 | pub maxx: c_float, 148 | } 149 | 150 | extern "C" { 151 | #[cfg(target_os = "windows")] 152 | pub fn gladLoadGL() -> i32; 153 | pub fn nvgBeginFrame( 154 | ctx: *mut NVGcontext, 155 | windowWidth: c_float, 156 | windowHeight: c_float, 157 | devicePixelRatio: c_float, 158 | ); 159 | pub fn nvgCancelFrame(ctx: *mut NVGcontext); 160 | pub fn nvgEndFrame(ctx: *mut NVGcontext); 161 | pub fn nvgGlobalCompositeOperation(ctx: *mut NVGcontext, op: c_int); 162 | pub fn nvgGlobalCompositeBlendFunc(ctx: *mut NVGcontext, sfactor: c_int, dfactor: c_int); 163 | pub fn nvgGlobalCompositeBlendFuncSeparate( 164 | ctx: *mut NVGcontext, 165 | srcRGB: c_int, 166 | dstRGB: c_int, 167 | srcAlpha: c_int, 168 | dstAlpha: c_int, 169 | ); 170 | pub fn nvgRGB(r: c_uchar, g: c_uchar, b: c_uchar) -> NVGcolor; 171 | pub fn nvgRGBf(r: c_float, g: c_float, b: c_float) -> NVGcolor; 172 | pub fn nvgRGBA(r: c_uchar, g: c_uchar, b: c_uchar, a: c_uchar) -> NVGcolor; 173 | pub fn nvgRGBAf(r: c_float, g: c_float, b: c_float, a: c_float) -> NVGcolor; 174 | pub fn nvgLerpRGBA(c0: NVGcolor, c1: NVGcolor, u: c_float) -> NVGcolor; 175 | pub fn nvgTransRGBA(c0: NVGcolor, a: c_uchar) -> NVGcolor; 176 | pub fn nvgTransRGBAf(c0: NVGcolor, a: c_float) -> NVGcolor; 177 | pub fn nvgHSL(h: c_float, s: c_float, l: c_float) -> NVGcolor; 178 | pub fn nvgHSLA(h: c_float, s: c_float, l: c_float, a: c_uchar) -> NVGcolor; 179 | pub fn nvgSave(ctx: *mut NVGcontext); 180 | pub fn nvgRestore(ctx: *mut NVGcontext); 181 | pub fn nvgReset(ctx: *mut NVGcontext); 182 | pub fn nvgShapeAntiAlias(ctx: *mut NVGcontext, enabled: c_int); 183 | pub fn nvgStrokeColor(ctx: *mut NVGcontext, color: NVGcolor); 184 | pub fn nvgStrokePaint(ctx: *mut NVGcontext, paint: NVGpaint); 185 | pub fn nvgFillColor(ctx: *mut NVGcontext, color: NVGcolor); 186 | pub fn nvgFillPaint(ctx: *mut NVGcontext, paint: NVGpaint); 187 | pub fn nvgMiterLimit(ctx: *mut NVGcontext, limit: c_float); 188 | pub fn nvgStrokeWidth(ctx: *mut NVGcontext, size: c_float); 189 | pub fn nvgLineCap(ctx: *mut NVGcontext, cap: c_int); 190 | pub fn nvgLineJoin(ctx: *mut NVGcontext, join: c_int); 191 | pub fn nvgGlobalAlpha(ctx: *mut NVGcontext, alpha: c_float); 192 | pub fn nvgResetTransform(ctx: *mut NVGcontext); 193 | pub fn nvgTransform( 194 | ctx: *mut NVGcontext, 195 | a: c_float, 196 | b: c_float, 197 | c: c_float, 198 | d: c_float, 199 | e: c_float, 200 | f: c_float, 201 | ); 202 | pub fn nvgTranslate(ctx: *mut NVGcontext, x: c_float, y: c_float); 203 | pub fn nvgRotate(ctx: *mut NVGcontext, angle: c_float); 204 | pub fn nvgSkewX(ctx: *mut NVGcontext, angle: c_float); 205 | pub fn nvgSkewY(ctx: *mut NVGcontext, angle: c_float); 206 | pub fn nvgScale(ctx: *mut NVGcontext, x: c_float, y: c_float); 207 | pub fn nvgCurrentTransform(ctx: *mut NVGcontext, xform: *mut c_float); 208 | pub fn nvgTransformIdentity(dst: *mut c_float); 209 | pub fn nvgTransformTranslate(dst: *mut c_float, tx: c_float, ty: c_float); 210 | pub fn nvgTransformScale(dst: *mut c_float, sx: c_float, sy: c_float); 211 | pub fn nvgTransformRotate(dst: *mut c_float, a: c_float); 212 | pub fn nvgTransformSkewX(dst: *mut c_float, a: c_float); 213 | pub fn nvgTransformSkewY(dst: *mut c_float, a: c_float); 214 | pub fn nvgTransformMultiply(dst: *mut c_float, src: *const c_float); 215 | pub fn nvgTransformPremultiply(dst: *mut c_float, src: *const c_float); 216 | pub fn nvgTransformInverse(dst: *mut c_float, src: *const c_float) -> c_int; 217 | pub fn nvgTransformPoint( 218 | dstx: *mut c_float, 219 | dsty: *mut c_float, 220 | xform: *const c_float, 221 | srcx: c_float, 222 | srcy: c_float, 223 | ); 224 | // Degrees <-> radians conversion functions are not exported. Use the rust builtins instead. 225 | pub fn nvgCreateImage( 226 | ctx: *mut NVGcontext, 227 | filename: *const c_char, 228 | imageFlags: c_int, 229 | ) -> c_int; 230 | pub fn nvgCreateImageMem( 231 | ctx: *mut NVGcontext, 232 | imageFlags: c_int, 233 | data: *mut c_uchar, 234 | ndata: c_int, 235 | ) -> c_int; 236 | pub fn nvgCreateImageRGBA( 237 | ctx: *mut NVGcontext, 238 | w: c_int, 239 | h: c_int, 240 | imageFlags: c_int, 241 | data: *const c_uchar, 242 | ) -> c_int; 243 | pub fn nvgUpdateImage(ctx: *mut NVGcontext, image: c_int, data: *const c_uchar); 244 | pub fn nvgImageSize(ctx: *mut NVGcontext, image: c_int, w: *mut c_int, h: *mut c_int); 245 | pub fn nvgDeleteImage(ctx: *mut NVGcontext, image: c_int); 246 | pub fn nvgLinearGradient( 247 | ctx: *mut NVGcontext, 248 | sx: c_float, 249 | sy: c_float, 250 | ex: c_float, 251 | ey: c_float, 252 | icol: NVGcolor, 253 | ocol: NVGcolor, 254 | ) -> NVGpaint; 255 | pub fn nvgBoxGradient( 256 | ctx: *mut NVGcontext, 257 | x: c_float, 258 | y: c_float, 259 | w: c_float, 260 | h: c_float, 261 | r: c_float, 262 | f: c_float, 263 | icol: NVGcolor, 264 | ocol: NVGcolor, 265 | ) -> NVGpaint; 266 | pub fn nvgRadialGradient( 267 | ctx: *mut NVGcontext, 268 | cx: c_float, 269 | cy: c_float, 270 | inr: c_float, 271 | outr: c_float, 272 | icol: NVGcolor, 273 | ocol: NVGcolor, 274 | ) -> NVGpaint; 275 | pub fn nvgImagePattern( 276 | ctx: *mut NVGcontext, 277 | ox: c_float, 278 | oy: c_float, 279 | ex: c_float, 280 | ey: c_float, 281 | angle: c_float, 282 | image: c_int, 283 | alpha: c_float, 284 | ) -> NVGpaint; 285 | pub fn nvgScissor(ctx: *mut NVGcontext, x: c_float, y: c_float, w: c_float, h: c_float); 286 | pub fn nvgIntersectScissor( 287 | ctx: *mut NVGcontext, 288 | x: c_float, 289 | y: c_float, 290 | w: c_float, 291 | h: c_float, 292 | ); 293 | pub fn nvgResetScissor(ctx: *mut NVGcontext); 294 | pub fn nvgBeginPath(ctx: *mut NVGcontext); 295 | pub fn nvgMoveTo(ctx: *mut NVGcontext, x: c_float, y: c_float); 296 | pub fn nvgLineTo(ctx: *mut NVGcontext, x: c_float, y: c_float); 297 | pub fn nvgBezierTo( 298 | ctx: *mut NVGcontext, 299 | c1x: c_float, 300 | c1y: c_float, 301 | c2x: c_float, 302 | c2y: c_float, 303 | x: c_float, 304 | y: c_float, 305 | ); 306 | pub fn nvgQuadTo(ctx: *mut NVGcontext, cx: c_float, cy: c_float, x: c_float, y: c_float); 307 | pub fn nvgArcTo( 308 | ctx: *mut NVGcontext, 309 | x1: c_float, 310 | y1: c_float, 311 | x2: c_float, 312 | y2: c_float, 313 | radius: c_float, 314 | ); 315 | pub fn nvgClosePath(ctx: *mut NVGcontext); 316 | pub fn nvgPathWinding(ctx: *mut NVGcontext, dir: c_int); 317 | pub fn nvgArc( 318 | ctx: *mut NVGcontext, 319 | cx: c_float, 320 | cy: c_float, 321 | r: c_float, 322 | a0: c_float, 323 | a1: c_float, 324 | dir: c_int, 325 | ); 326 | pub fn nvgRect(ctx: *mut NVGcontext, x: c_float, y: c_float, w: c_float, h: c_float); 327 | pub fn nvgRoundedRect( 328 | ctx: *mut NVGcontext, 329 | x: c_float, 330 | y: c_float, 331 | w: c_float, 332 | h: c_float, 333 | r: c_float, 334 | ); 335 | pub fn nvgRoundedRectVarying( 336 | ctx: *mut NVGcontext, 337 | x: c_float, 338 | y: c_float, 339 | w: c_float, 340 | h: c_float, 341 | radTopLeft: c_float, 342 | radTopRight: c_float, 343 | radBottomRight: c_float, 344 | radBottomLeft: c_float, 345 | ); 346 | pub fn nvgEllipse(ctx: *mut NVGcontext, cx: c_float, cy: c_float, rx: c_float, ry: c_float); 347 | pub fn nvgCircle(ctx: *mut NVGcontext, cx: c_float, cy: c_float, r: c_float); 348 | pub fn nvgFill(ctx: *mut NVGcontext); 349 | pub fn nvgStroke(ctx: *mut NVGcontext); 350 | pub fn nvgCreateFont( 351 | ctx: *mut NVGcontext, 352 | name: *const c_char, 353 | filename: *const c_char, 354 | ) -> c_int; 355 | pub fn nvgCreateFontMem( 356 | ctx: *mut NVGcontext, 357 | name: *const c_char, 358 | data: *mut c_uchar, 359 | ndata: c_int, 360 | freeData: c_int, 361 | ) -> c_int; 362 | pub fn nvgFindFont(ctx: *mut NVGcontext, name: *const c_char) -> c_int; 363 | pub fn nvgAddFallbackFontId( 364 | ctx: *mut NVGcontext, 365 | baseFont: c_int, 366 | fallbackFont: c_int, 367 | ) -> c_int; 368 | pub fn nvgAddFallbackFont( 369 | ctx: *mut NVGcontext, 370 | baseFont: *const c_char, 371 | fallbackFont: *const c_char, 372 | ) -> c_int; 373 | pub fn nvgFontSize(ctx: *mut NVGcontext, size: c_float); 374 | pub fn nvgFontBlur(ctx: *mut NVGcontext, blur: c_float); 375 | pub fn nvgTextLetterSpacing(ctx: *mut NVGcontext, spacing: c_float); 376 | pub fn nvgTextLineHeight(ctx: *mut NVGcontext, lineHeight: c_float); 377 | pub fn nvgTextAlign(ctx: *mut NVGcontext, align: c_int); 378 | pub fn nvgFontFaceId(ctx: *mut NVGcontext, font: c_int); 379 | pub fn nvgFontFace(ctx: *mut NVGcontext, font: *const c_char); 380 | pub fn nvgText( 381 | ctx: *mut NVGcontext, 382 | x: c_float, 383 | y: c_float, 384 | string: *const c_char, 385 | end: *const c_char, 386 | ) -> c_float; 387 | pub fn nvgTextBox( 388 | ctx: *mut NVGcontext, 389 | x: c_float, 390 | y: c_float, 391 | breakRowWidth: c_float, 392 | string: *const c_char, 393 | end: *const c_char, 394 | ); 395 | pub fn nvgTextBounds( 396 | ctx: *mut NVGcontext, 397 | x: c_float, 398 | y: c_float, 399 | string: *const c_char, 400 | end: *const c_char, 401 | bounds: *mut c_float, 402 | ) -> c_float; 403 | pub fn nvgTextBoxBounds( 404 | ctx: *mut NVGcontext, 405 | x: c_float, 406 | y: c_float, 407 | breakRowWidth: c_float, 408 | string: *const c_char, 409 | end: *const c_char, 410 | bounds: *mut c_float, 411 | ); 412 | pub fn nvgTextGlyphPositions( 413 | ctx: *mut NVGcontext, 414 | x: c_float, 415 | y: c_float, 416 | string: *const c_char, 417 | end: *const c_char, 418 | positions: *mut NVGglyphPosition, 419 | maxPositions: c_int, 420 | ) -> c_int; 421 | pub fn nvgTextMetrics( 422 | ctx: *mut NVGcontext, 423 | ascender: *mut c_float, 424 | descender: *mut c_float, 425 | lineh: *mut c_float, 426 | ); 427 | pub fn nvgTextBreakLines( 428 | ctx: *mut NVGcontext, 429 | string: *const c_char, 430 | end: *const c_char, 431 | breakRowWidth: c_float, 432 | rows: *mut NVGtextRow, 433 | maxRows: c_int, 434 | ) -> c_int; 435 | #[cfg(feature = "gl2")] 436 | pub fn nvgCreateGL2(flags: c_int) -> *mut NVGcontext; 437 | #[cfg(feature = "gl2")] 438 | pub fn nvgDeleteGL2(ctx: *mut NVGcontext); 439 | #[cfg(feature = "gl3")] 440 | pub fn nvgCreateGL3(flags: c_int) -> *mut NVGcontext; 441 | #[cfg(feature = "gl3")] 442 | pub fn nvgDeleteGL3(ctx: *mut NVGcontext); 443 | #[cfg(feature = "gles2")] 444 | pub fn nvgCreateGLES2(flags: c_int) -> *mut NVGcontext; 445 | #[cfg(feature = "gles2")] 446 | pub fn nvgDeleteGLES2(ctx: *mut NVGcontext); 447 | #[cfg(feature = "gles3")] 448 | pub fn nvgCreateGLES3(flags: c_int) -> *mut NVGcontext; 449 | #[cfg(feature = "gles3")] 450 | pub fn nvgDeleteGLES3(ctx: *mut NVGcontext); 451 | } 452 | -------------------------------------------------------------------------------- /nanovg-sys/nanovg_shim.c: -------------------------------------------------------------------------------- 1 | #if defined(__APPLE__) 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #elif defined(_WIN32) 8 | #include 9 | #else 10 | #include 11 | #endif 12 | 13 | #include 14 | #include -------------------------------------------------------------------------------- /resources/Mechanic of the Heart.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/Mechanic of the Heart.ttf -------------------------------------------------------------------------------- /resources/NotoEmoji-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/NotoEmoji-Regular.ttf -------------------------------------------------------------------------------- /resources/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/Roboto-Bold.ttf -------------------------------------------------------------------------------- /resources/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/Roboto-Regular.ttf -------------------------------------------------------------------------------- /resources/entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/entypo.ttf -------------------------------------------------------------------------------- /resources/images/image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/images/image1.jpg -------------------------------------------------------------------------------- /resources/images/image10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/images/image10.jpg -------------------------------------------------------------------------------- /resources/images/image11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/images/image11.jpg -------------------------------------------------------------------------------- /resources/images/image12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/images/image12.jpg -------------------------------------------------------------------------------- /resources/images/image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/images/image2.jpg -------------------------------------------------------------------------------- /resources/images/image3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/images/image3.jpg -------------------------------------------------------------------------------- /resources/images/image4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/images/image4.jpg -------------------------------------------------------------------------------- /resources/images/image5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/images/image5.jpg -------------------------------------------------------------------------------- /resources/images/image6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/images/image6.jpg -------------------------------------------------------------------------------- /resources/images/image7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/images/image7.jpg -------------------------------------------------------------------------------- /resources/images/image8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/images/image8.jpg -------------------------------------------------------------------------------- /resources/images/image9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/images/image9.jpg -------------------------------------------------------------------------------- /resources/lenna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/resources/lenna.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | 3 | -------------------------------------------------------------------------------- /screenshots/README.md: -------------------------------------------------------------------------------- 1 | # Screenshots 2 | 3 | ![demo-clock](demo-clock.png) 4 | Output of the `demo-clock` example. 5 | 6 | ![demo-ui](demo-ui.png) 7 | Output of the `demo-ui` example. 8 | 9 | ![demo-glutin](demo-glutin.png) 10 | Output of the `demo-glutin` example. 11 | 12 | ![demo-cutout](demo-cutout.gif) 13 | Output of the `demo-cutout` example. 14 | 15 | ![demo-text](demo-text.png) 16 | Output of the `demo-text` example. 17 | 18 | ![demo-transform](demo-transform.png) 19 | Output of the `demo-transform` example. -------------------------------------------------------------------------------- /screenshots/demo-clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/screenshots/demo-clock.png -------------------------------------------------------------------------------- /screenshots/demo-cutout.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/screenshots/demo-cutout.gif -------------------------------------------------------------------------------- /screenshots/demo-glutin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/screenshots/demo-glutin.png -------------------------------------------------------------------------------- /screenshots/demo-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/screenshots/demo-text.png -------------------------------------------------------------------------------- /screenshots/demo-transform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/screenshots/demo-transform.png -------------------------------------------------------------------------------- /screenshots/demo-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinKelley/nanovg-rs/722dce9994a3612c9f771d7cf35dfc55abd53623/screenshots/demo-ui.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate nanovg_sys as ffi; 2 | 3 | use std::ffi::{CString, NulError}; 4 | use std::ops::Drop; 5 | use std::os::raw::{c_char, c_float, c_int, c_uchar}; 6 | use std::path::Path as IoPath; 7 | use std::{mem, ptr}; 8 | 9 | #[cfg(target_os = "windows")] 10 | fn init_gl() -> Result<(), ()> { 11 | if unsafe { ffi::gladLoadGL() } == 1 { 12 | Ok(()) 13 | } else { 14 | Err(()) 15 | } 16 | } 17 | 18 | #[cfg(not(target_os = "windows"))] 19 | fn init_gl() -> Result<(), ()> { 20 | Ok(()) 21 | } 22 | 23 | #[cfg(feature = "gl3")] 24 | fn create_gl(flags: ffi::NVGcreateFlags) -> *mut ffi::NVGcontext { 25 | unsafe { ffi::nvgCreateGL3(flags.bits()) } 26 | } 27 | 28 | #[cfg(feature = "gl2")] 29 | fn create_gl(flags: ffi::NVGcreateFlags) -> *mut ffi::NVGcontext { 30 | unsafe { ffi::nvgCreateGL2(flags.bits()) } 31 | } 32 | 33 | #[cfg(feature = "gles3")] 34 | fn create_gl(flags: ffi::NVGcreateFlags) -> *mut ffi::NVGcontext { 35 | unsafe { ffi::nvgCreateGLES3(flags.bits()) } 36 | } 37 | 38 | #[cfg(feature = "gles2")] 39 | fn create_gl(flags: ffi::NVGcreateFlags) -> *mut ffi::NVGcontext { 40 | unsafe { ffi::nvgCreateGLES2(flags.bits()) } 41 | } 42 | 43 | #[cfg(not(any(feature = "gl3", feature = "gl2", feature = "gles3", feature = "gles2")))] 44 | fn create_gl(flags: ffi::NVGcreateFlags) -> *mut ffi::NVGcontext { 45 | panic!("Unable to determine the backend / implementation. Have you enabled one of the features?") 46 | } 47 | 48 | /// A builder that configures and constructs a NanoVG context. 49 | #[derive(Debug)] 50 | pub struct ContextBuilder { 51 | flags: ffi::NVGcreateFlags, 52 | } 53 | 54 | impl ContextBuilder { 55 | /// Create a new context builder. 56 | pub fn new() -> Self { 57 | Self { 58 | flags: ffi::NVGcreateFlags::empty(), 59 | } 60 | } 61 | 62 | /// Enable antialiased rasterization. Not needed if you have multisampling enabled. 63 | pub fn antialias(mut self) -> Self { 64 | self.flags.insert(ffi::NVGcreateFlags::NVG_ANTIALIAS); 65 | self 66 | } 67 | 68 | /// Enable stencil strokes. Overlapping, stroked paths will only be drawn once, for a little performance loss. 69 | pub fn stencil_strokes(mut self) -> Self { 70 | self.flags.insert(ffi::NVGcreateFlags::NVG_STENCIL_STROKES); 71 | self 72 | } 73 | 74 | /// Enable additional debug checks. 75 | pub fn debug(mut self) -> Self { 76 | self.flags.insert(ffi::NVGcreateFlags::NVG_DEBUG); 77 | self 78 | } 79 | 80 | /// Construct the context. 81 | /// Make sure you have enabled one of the 4 OpenGL features, or this function will panic. 82 | pub fn build(self) -> Result { 83 | init_gl()?; 84 | let raw = create_gl(self.flags); 85 | if !raw.is_null() { 86 | Ok(Context(raw)) 87 | } else { 88 | Err(()) 89 | } 90 | } 91 | } 92 | 93 | /// A initialized NanoVG context - the central type which all operations rely on. 94 | #[derive(Debug)] 95 | pub struct Context(*mut ffi::NVGcontext); 96 | 97 | impl Context { 98 | /// Return the raw FFI C-struct pointer to the context. 99 | pub fn raw(&self) -> *mut ffi::NVGcontext { 100 | self.0 101 | } 102 | 103 | /// Begin drawing a frame. 104 | /// All NanoVG drawing takes place within a frame. 105 | /// 106 | /// `width` and `height` should be the width and height of the framebuffer / window client size. 107 | /// `device_pixel_ratio` defines the pixel ratio. NanoVG doesn't guess this automatically to allow for Hi-DPI devices. 108 | /// Basically, this is your hidpi factor. 109 | /// `handler` is the callback in which you draw your paths. You cannot draw paths outside of this callback. 110 | pub fn frame<'a, F: FnOnce(Frame<'a>)>(&'a self, (width, height): (f32, f32), device_pixel_ratio: f32, handler: F) { 111 | unsafe { 112 | ffi::nvgBeginFrame( 113 | self.raw(), 114 | width as c_float, 115 | height as c_float, 116 | device_pixel_ratio as c_float, 117 | ); 118 | } 119 | { 120 | let frame = Frame::new(self, Transform::new()); 121 | handler(frame); 122 | } 123 | unsafe { 124 | ffi::nvgEndFrame(self.raw()); 125 | } 126 | } 127 | 128 | fn global_composite_operation(&self, operation: CompositeOperation) { 129 | let ctx = self.raw(); 130 | match operation { 131 | CompositeOperation::Basic(basic) => unsafe { 132 | ffi::nvgGlobalCompositeOperation(ctx, basic.into_raw() as c_int); 133 | }, 134 | CompositeOperation::BlendFunc { 135 | source: src, 136 | destination: dst, 137 | } => unsafe { 138 | ffi::nvgGlobalCompositeBlendFunc(ctx, src.into_raw().bits(), dst.into_raw().bits()); 139 | }, 140 | CompositeOperation::BlendFuncSeparate { 141 | rgb_source: rs, 142 | rgb_destination: rd, 143 | alpha_source: als, 144 | alpha_destination: ald, 145 | } => unsafe { 146 | let (rs, rd, als, ald) = ( 147 | rs.into_raw().bits(), 148 | rd.into_raw().bits(), 149 | als.into_raw().bits(), 150 | ald.into_raw().bits(), 151 | ); 152 | ffi::nvgGlobalCompositeBlendFuncSeparate(ctx, rs, rd, als, ald); 153 | }, 154 | } 155 | } 156 | 157 | fn global_alpha(&self, alpha: f32) { 158 | unsafe { 159 | ffi::nvgGlobalAlpha(self.raw(), alpha as c_float); 160 | } 161 | } 162 | 163 | fn scissor(&self, scissor: Option) { 164 | match scissor { 165 | Some(ref scissor) => { 166 | self.with_applied_transform(scissor.transform, || unsafe { 167 | ffi::nvgScissor(self.raw(), scissor.x, scissor.y, scissor.width, scissor.height); 168 | }); 169 | } 170 | None => unsafe { 171 | ffi::nvgResetScissor(self.raw()); 172 | }, 173 | } 174 | } 175 | 176 | fn intersect(&self, intersect: &Intersect) { 177 | self.scissor(Some(intersect.with)); 178 | 179 | self.with_applied_transform(intersect.transform, || unsafe { 180 | ffi::nvgIntersectScissor(self.raw(), intersect.x, intersect.y, intersect.width, intersect.height); 181 | }); 182 | } 183 | 184 | fn clip(&self, clip: Clip) { 185 | match clip { 186 | Clip::Scissor(scissor) => self.scissor(Some(scissor)), 187 | Clip::Intersect(ref intersect) => self.intersect(intersect), 188 | Clip::None => (), 189 | } 190 | } 191 | 192 | fn transform(&self, transform: Option) { 193 | match transform { 194 | Some(ref transform) => { 195 | let t = transform.matrix; 196 | unsafe { 197 | ffi::nvgTransform(self.raw(), t[0], t[1], t[2], t[3], t[4], t[5]); 198 | } 199 | } 200 | None => unsafe { 201 | ffi::nvgResetTransform(self.raw()); 202 | }, 203 | } 204 | } 205 | 206 | fn current_transform(&self) -> Transform { 207 | let mut current = Transform::new(); 208 | unsafe { 209 | ffi::nvgCurrentTransform(self.raw(), current.matrix.as_mut_ptr()); 210 | } 211 | current 212 | } 213 | 214 | fn with_applied_transform(&self, transform: Option, handler: F) { 215 | let current = self.current_transform(); 216 | 217 | if let Some(transform) = transform { 218 | if transform.absolute { 219 | self.transform(None); 220 | } 221 | 222 | self.transform(Some(transform)); 223 | } 224 | 225 | handler(); 226 | 227 | self.transform(None); 228 | self.transform(Some(current)); 229 | } 230 | } 231 | 232 | impl Drop for Context { 233 | #[cfg(feature = "gl3")] 234 | fn drop(&mut self) { 235 | unsafe { 236 | ffi::nvgDeleteGL3(self.0); 237 | } 238 | } 239 | 240 | #[cfg(feature = "gl2")] 241 | fn drop(&mut self) { 242 | unsafe { 243 | ffi::nvgDeleteGL2(self.0); 244 | } 245 | } 246 | 247 | #[cfg(feature = "gles3")] 248 | fn drop(&mut self) { 249 | unsafe { 250 | ffi::nvgDeleteGLES3(self.0); 251 | } 252 | } 253 | 254 | #[cfg(feature = "gles2")] 255 | fn drop(&mut self) { 256 | unsafe { 257 | ffi::nvgDeleteGLES2(self.0); 258 | } 259 | } 260 | 261 | #[cfg(not(any(feature = "gl3", feature = "gl2", feature = "gles3", feature = "gles2")))] 262 | fn drop(&mut self) {} 263 | } 264 | 265 | /// A scissor defines a region on the screen in which drawing operations are allowed. 266 | /// Pixels drawn outside of this region are clipped. 267 | #[derive(Clone, Copy, Debug, PartialEq)] 268 | pub struct Scissor { 269 | pub x: f32, 270 | pub y: f32, 271 | pub width: f32, 272 | pub height: f32, 273 | pub transform: Option, 274 | } 275 | 276 | /// Define intersection scissor which gets intersected with 'with' Scissor. 277 | /// Pixels drawn outside of this intersection are clipped. 278 | /// When 'with' Scissor or this Intersection have rotation, the intersection will be an approximation. 279 | #[derive(Clone, Copy, Debug, PartialEq)] 280 | pub struct Intersect { 281 | pub x: f32, 282 | pub y: f32, 283 | pub width: f32, 284 | pub height: f32, 285 | pub with: Scissor, 286 | pub transform: Option, 287 | } 288 | 289 | /// Define how to clip specified region. 290 | #[derive(Clone, Copy, Debug, PartialEq)] 291 | pub enum Clip { 292 | Scissor(Scissor), 293 | Intersect(Intersect), 294 | None, 295 | } 296 | 297 | /// Options which control how a path is rendered. 298 | #[derive(Clone, Copy, Debug, PartialEq)] 299 | pub struct PathOptions { 300 | /// The clip defines the rectangular region in which the frame is clipped into. 301 | /// All overflowing pixels will be discarded. 302 | pub clip: Clip, 303 | /// Defines how overlapping paths are composited together. 304 | pub composite_operation: CompositeOperation, 305 | /// The alpha component of the path. 306 | pub alpha: f32, 307 | /// A transformation which 'transforms' the coordinate system and consequently the path. 308 | pub transform: Option, 309 | } 310 | 311 | impl Default for PathOptions { 312 | fn default() -> Self { 313 | Self { 314 | clip: Clip::None, 315 | composite_operation: CompositeOperation::Basic(BasicCompositeOperation::Atop), 316 | alpha: 1.0, 317 | transform: None, 318 | } 319 | } 320 | } 321 | 322 | /// A frame which can draw paths. 323 | /// All NanoVG path drawing operations are done on a frame. 324 | #[derive(Debug)] 325 | pub struct Frame<'a> { 326 | context: &'a Context, 327 | transform: Transform, 328 | } 329 | 330 | impl<'a> Frame<'a> { 331 | fn new(context: &'a Context, transform: Transform) -> Self { 332 | Self { context, transform } 333 | } 334 | 335 | /// Get the underlying context this frame was created on. 336 | pub fn context(&self) -> &'a Context { 337 | self.context 338 | } 339 | 340 | /// Get current transform which the frame is transformed by. 341 | pub fn transform(&self) -> &Transform { 342 | &self.transform 343 | } 344 | 345 | /// Create a new frame where all drawing operations are transformed by `transform`. 346 | /// 347 | /// `self` is passed as &mut because only one transformation at a time can be applied to a single frame. 348 | /// Transforming a sub-frame however, is valid. 349 | /// The resulting transformation can be retrieved with [Frame::transform]. 350 | pub fn transformed<'b, F: FnOnce(Frame<'b>)>(&'b mut self, transform: Transform, handler: F) { 351 | let frame = Frame::new(self.context, transform * self.transform); 352 | handler(frame); 353 | } 354 | 355 | /// Draw a new path. 356 | /// 357 | /// `handler` is the callback in which you operate the path. 358 | /// `options` control how the path is rendered. 359 | pub fn path(&self, handler: F, options: PathOptions) { 360 | self.context.global_composite_operation(options.composite_operation); 361 | self.context.global_alpha(options.alpha); 362 | 363 | self.draw_prepare(options.clip, options.transform); 364 | 365 | unsafe { 366 | ffi::nvgBeginPath(self.context.raw()); 367 | } 368 | handler(Path::new(self)); 369 | } 370 | 371 | fn draw_prepare(&self, clip: Clip, transform: Option) { 372 | self.context.transform(None); 373 | self.context.scissor(None); 374 | self.context.transform(Some(self.transform)); 375 | 376 | self.context.clip(clip); 377 | 378 | if let Some(transform) = transform { 379 | if transform.absolute { 380 | self.context.transform(None); 381 | } 382 | self.context.transform(Some(transform)); 383 | } 384 | } 385 | 386 | fn text_prepare(&self, font: Font, options: TextOptions) { 387 | unsafe { 388 | ffi::nvgFontFaceId(self.context.raw(), font.id()); 389 | ffi::nvgFillColor(self.context.raw(), options.color.into_raw()); 390 | ffi::nvgFontSize(self.context.raw(), options.size); 391 | ffi::nvgFontBlur(self.context.raw(), options.blur); 392 | ffi::nvgTextLetterSpacing(self.context.raw(), options.letter_spacing); 393 | ffi::nvgTextLineHeight(self.context.raw(), options.line_height); 394 | ffi::nvgTextAlign(self.context.raw(), options.align.into_raw().bits()); 395 | } 396 | } 397 | 398 | /// Draw a single line on the screen. Newline characters are ignored. 399 | /// `font` the font face to use. 400 | /// `(x, y)` the origin / position to draw the text at. The origin is relative to the alignment of `options`. 401 | /// `text` the string to draw. 402 | /// `options` optional (`Default::default`) options that control the visual appearance of the text. 403 | pub fn text>(&self, font: Font, (x, y): (f32, f32), text: S, options: TextOptions) { 404 | let text = CString::new(text.as_ref()).unwrap(); 405 | self.text_prepare(font, options); 406 | 407 | self.draw_prepare(options.clip, options.transform); 408 | 409 | unsafe { 410 | ffi::nvgText(self.context.raw(), x, y, text.as_ptr(), ptr::null()); 411 | } 412 | } 413 | 414 | /// Draw multiline text on the screen. 415 | /// `font` the font face to use. 416 | /// `(x, y)` the origin / position to draw the text at. The origin is relative to the alignment of `options`. 417 | /// `text` the string to draw. 418 | /// `options` optional (`Default::default`) options that control the visual appearance of the text. 419 | pub fn text_box>(&self, font: Font, (x, y): (f32, f32), text: S, options: TextOptions) { 420 | let text = CString::new(text.as_ref()).unwrap(); 421 | self.text_prepare(font, options); 422 | 423 | self.draw_prepare(options.clip, options.transform); 424 | 425 | unsafe { 426 | ffi::nvgTextBox( 427 | self.context.raw(), 428 | x, 429 | y, 430 | options.line_max_width, 431 | text.as_ptr(), 432 | ptr::null(), 433 | ); 434 | } 435 | } 436 | 437 | /// Measures specified text string. 438 | /// Returns tuple (f32, TextBounds) where the first element specifies horizontal advance of measured text 439 | /// and the second element specifies the bounding box of measured text. 440 | /// `font` the font face to use. 441 | /// `(x, y)` the origin / position to measure the text from. 442 | /// `text` the string to measure. 443 | /// `options` optional (`Default::default`) options that controls how the text is measured. 444 | pub fn text_bounds>( 445 | &self, 446 | font: Font, 447 | (x, y): (f32, f32), 448 | text: S, 449 | options: TextOptions, 450 | ) -> (f32, TextBounds) { 451 | let text = CString::new(text.as_ref()).unwrap(); 452 | self.text_prepare(font, options); 453 | let mut bounds = [0.0f32; 4]; 454 | let measure = unsafe { 455 | ffi::nvgTextBounds( 456 | self.context.raw(), 457 | x, 458 | y, 459 | text.as_ptr(), 460 | ptr::null(), 461 | bounds.as_mut_ptr(), 462 | ) 463 | }; 464 | (measure, TextBounds::new(&bounds)) 465 | } 466 | 467 | /// Measures specified multi-text string. 468 | /// Returns bounding box of measured multi-text. 469 | /// `font` the font face to use. 470 | /// `(x, y)` the origin / position to measure the text from. 471 | /// `text` the string to measure. 472 | /// `options` optional (`Default::default`) options that controls how the text is measured. 473 | pub fn text_box_bounds>( 474 | &self, 475 | font: Font, 476 | (x, y): (f32, f32), 477 | text: S, 478 | options: TextOptions, 479 | ) -> TextBounds { 480 | let text = CString::new(text.as_ref()).unwrap(); 481 | self.text_prepare(font, options); 482 | let mut bounds = [0.0f32; 4]; 483 | unsafe { 484 | ffi::nvgTextBoxBounds( 485 | self.context.raw(), 486 | x, 487 | y, 488 | options.line_max_width, 489 | text.as_ptr(), 490 | ptr::null(), 491 | bounds.as_mut_ptr(), 492 | ); 493 | } 494 | TextBounds::new(&bounds) 495 | } 496 | 497 | /// Calculates and breaks text into series of glyph positions. 498 | /// Returns iterator over all glyph positions in text. 499 | /// `(x, y)` the coordinate space from which to offset coordinates in `GlyphPosition` 500 | /// `text` the text to break into glyph positions 501 | /// If you want to get current glyph and the next one, use .peekable() on this iterator. 502 | pub fn text_glyph_positions>(&self, (x, y): (f32, f32), text: S) -> TextGlyphPositions { 503 | TextGlyphPositions::new(self.context, x, y, CString::new(text.as_ref()).unwrap()) 504 | } 505 | 506 | /// Returns vertical text metrics based on given font and text options 507 | /// Measured values are stored in TextMetrics struct in local coordinate space. 508 | /// `options` the options specify how metrics should be calculated. 509 | /// `font` the font for which to calculate metrics. 510 | pub fn text_metrics(&self, font: Font, options: TextOptions) -> TextMetrics { 511 | self.text_prepare(font, options); 512 | let mut metrics = TextMetrics::new(); 513 | unsafe { 514 | ffi::nvgTextMetrics( 515 | self.context.raw(), 516 | &mut metrics.ascender, 517 | &mut metrics.descender, 518 | &mut metrics.line_height, 519 | ); 520 | } 521 | metrics 522 | } 523 | 524 | /// Breaks text into lines. 525 | /// Text is split at word boundaries, new-line character or when row width exceeds break_row_width. 526 | /// Returns iterator over text lines. 527 | /// `font` the font face to use. 528 | /// `text` the text to break into lines 529 | /// `break_row_width` maximum width of row 530 | /// `options` optional (`Default::default`) options that controls how the text is broken into lines. 531 | pub fn text_break_lines>( 532 | &self, 533 | font: Font, 534 | text: S, 535 | break_row_width: f32, 536 | options: TextOptions, 537 | ) -> TextBreakLines { 538 | self.text_prepare(font, options); 539 | TextBreakLines::new(self.context, CString::new(text.as_ref()).unwrap(), break_row_width) 540 | } 541 | } 542 | 543 | /// A path, the main type for most NanoVG drawing operations. 544 | #[derive(Debug)] 545 | pub struct Path<'a, 'b> 546 | where 547 | 'b: 'a, 548 | { 549 | frame: &'a Frame<'b>, 550 | } 551 | 552 | impl<'a, 'b> Path<'a, 'b> { 553 | fn new(frame: &'a Frame<'b>) -> Self { 554 | Self { frame } 555 | } 556 | 557 | fn ctx(&self) -> *mut ffi::NVGcontext { 558 | self.frame.context.raw() 559 | } 560 | 561 | /// Get the underlying context this path was created on. 562 | pub fn context(&self) -> &'a Context { 563 | self.frame.context() 564 | } 565 | 566 | /// Draw the current path by filling in it's shape. 567 | /// 'paint' specifies in which color/paint should fill be drawn. 568 | /// pass variables that implement Paint trait 569 | /// for now these are: Color, Gradient, ImagePattern 570 | /// 'options' specifies how filling should be done. 571 | pub fn fill(&self, paint: T, options: FillOptions) { 572 | let ctx = self.ctx(); 573 | unsafe { 574 | ffi::nvgShapeAntiAlias(ctx, options.antialias as c_int); 575 | paint.fill(self.context()); 576 | ffi::nvgFill(ctx); 577 | } 578 | } 579 | 580 | /// Draw the current path by stroking it's perimeter. 581 | /// 'paint' specifies in which color/paint should stroke be drawn. 582 | /// pass variables that implement Paint trait 583 | /// for now these are: Color, Gradient, ImagePattern 584 | /// 'options' specifies how stroking should be done. 585 | pub fn stroke(&self, paint: T, options: StrokeOptions) { 586 | let ctx = self.ctx(); 587 | unsafe { 588 | ffi::nvgShapeAntiAlias(ctx, options.antialias as c_int); 589 | ffi::nvgStrokeWidth(ctx, options.width as c_float); 590 | ffi::nvgLineCap(ctx, options.line_cap.into_raw() as c_int); 591 | ffi::nvgLineJoin(ctx, options.line_join.into_raw() as c_int); 592 | ffi::nvgMiterLimit(ctx, options.miter_limit as c_float); 593 | paint.stroke(self.context()); 594 | ffi::nvgStroke(ctx); 595 | } 596 | } 597 | 598 | /// Add an arc to the path. 599 | pub fn arc(&self, (cx, cy): (f32, f32), radius: f32, start_angle: f32, end_angle: f32, winding: Winding) { 600 | unsafe { 601 | ffi::nvgArc(self.ctx(), cx, cy, radius, start_angle, end_angle, winding.into_raw()); 602 | } 603 | } 604 | 605 | /// Add a rectangle to the path. 606 | pub fn rect(&self, (x, y): (f32, f32), (w, h): (f32, f32)) { 607 | unsafe { 608 | ffi::nvgRect(self.ctx(), x as c_float, y as c_float, w as c_float, h as c_float); 609 | } 610 | } 611 | 612 | /// Add a rounded rectangle to the path. 613 | pub fn rounded_rect(&self, (x, y): (f32, f32), (w, h): (f32, f32), radius: f32) { 614 | unsafe { 615 | ffi::nvgRoundedRect(self.ctx(), x, y, w, h, radius); 616 | } 617 | } 618 | 619 | /// Add a rounded rectangle with varying corners to the path. 620 | /// `top_radii` and `bottom_radii` are both tuples in the form (left, right). 621 | pub fn rounded_rect_varying( 622 | &self, 623 | (x, y): (f32, f32), 624 | (w, h): (f32, f32), 625 | top_radii: (f32, f32), 626 | bottom_radii: (f32, f32), 627 | ) { 628 | unsafe { 629 | ffi::nvgRoundedRectVarying( 630 | self.ctx(), 631 | x, 632 | y, 633 | w, 634 | h, 635 | top_radii.0, 636 | top_radii.1, 637 | bottom_radii.1, 638 | bottom_radii.0, 639 | ); 640 | } 641 | } 642 | 643 | /// Add an ellipse to the path. 644 | pub fn ellipse(&self, (cx, cy): (f32, f32), radius_x: f32, radius_y: f32) { 645 | unsafe { 646 | ffi::nvgEllipse(self.ctx(), cx, cy, radius_x, radius_y); 647 | } 648 | } 649 | 650 | /// Add a circle to the path. 651 | pub fn circle(&self, (cx, cy): (f32, f32), radius: f32) { 652 | unsafe { 653 | ffi::nvgCircle(self.ctx(), cx, cy, radius); 654 | } 655 | } 656 | 657 | /// Add a line to the subpath. 658 | pub fn line_to(&self, (x, y): (f32, f32)) { 659 | unsafe { 660 | ffi::nvgLineTo(self.ctx(), x, y); 661 | } 662 | } 663 | 664 | /// Add a cubic bezier curve to the subpath. 665 | pub fn cubic_bezier_to(&self, (x, y): (f32, f32), control1: (f32, f32), control2: (f32, f32)) { 666 | unsafe { 667 | ffi::nvgBezierTo(self.ctx(), control1.0, control1.1, control2.0, control2.1, x, y); 668 | } 669 | } 670 | 671 | /// Add a quadratic bezier curve to the subpath. 672 | pub fn quad_bezier_to(&self, (x, y): (f32, f32), control: (f32, f32)) { 673 | unsafe { 674 | ffi::nvgQuadTo(self.ctx(), control.0, control.1, x, y); 675 | } 676 | } 677 | 678 | /// Add a arc to the subpath. 679 | pub fn arc_to(&self, p1: (f32, f32), p2: (f32, f32), radius: f32) { 680 | unsafe { 681 | ffi::nvgArcTo(self.ctx(), p1.0, p1.1, p2.0, p2.1, radius); 682 | } 683 | } 684 | 685 | /// Set the winding of the subpath. 686 | /// The winding defines which parts of the subpath are 'inside' and which are 'outside'. 687 | pub fn winding(&self, winding: Winding) { 688 | unsafe { 689 | ffi::nvgPathWinding(self.ctx(), winding.into_raw()); 690 | } 691 | } 692 | 693 | /// Start new sub-path with specified coordinates as the first point. 694 | pub fn move_to(&self, (x, y): (f32, f32)) { 695 | unsafe { 696 | ffi::nvgMoveTo(self.ctx(), x, y); 697 | } 698 | } 699 | 700 | /// Close the path, ie. connect the first point and last point with a line. 701 | pub fn close(&self) { 702 | unsafe { 703 | ffi::nvgClosePath(self.ctx()); 704 | } 705 | } 706 | } 707 | 708 | /// Controls how filling in a path should look. 709 | #[derive(Debug)] 710 | pub struct FillOptions { 711 | pub antialias: bool, 712 | } 713 | 714 | impl Default for FillOptions { 715 | fn default() -> Self { 716 | Self { antialias: true } 717 | } 718 | } 719 | 720 | /// Controls how stroking a path should look. 721 | #[derive(Debug, PartialEq)] 722 | pub struct StrokeOptions { 723 | pub width: f32, 724 | pub line_cap: LineCap, 725 | pub line_join: LineJoin, 726 | pub miter_limit: f32, 727 | pub antialias: bool, 728 | } 729 | 730 | impl Default for StrokeOptions { 731 | fn default() -> Self { 732 | Self { 733 | width: 1.0, 734 | line_cap: LineCap::Butt, 735 | line_join: LineJoin::Miter, 736 | miter_limit: 10.0, 737 | antialias: true, 738 | } 739 | } 740 | } 741 | 742 | /// Controls how the end of line is drawn. 743 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 744 | pub enum LineCap { 745 | Butt, 746 | Round, 747 | Square, 748 | } 749 | 750 | impl LineCap { 751 | fn into_raw(self) -> ffi::NVGlineCap { 752 | match self { 753 | LineCap::Butt => ffi::NVGlineCap::NVG_BUTT, 754 | LineCap::Round => ffi::NVGlineCap::NVG_ROUND, 755 | LineCap::Square => ffi::NVGlineCap::NVG_SQUARE, 756 | } 757 | } 758 | } 759 | 760 | /// Controls how lines are joined together. 761 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 762 | pub enum LineJoin { 763 | Miter, 764 | Round, 765 | Bevel, 766 | } 767 | 768 | impl LineJoin { 769 | fn into_raw(self) -> ffi::NVGlineCap { 770 | match self { 771 | LineJoin::Miter => ffi::NVGlineCap::NVG_MITER, 772 | LineJoin::Round => ffi::NVGlineCap::NVG_ROUND, 773 | LineJoin::Bevel => ffi::NVGlineCap::NVG_BEVEL, 774 | } 775 | } 776 | } 777 | 778 | pub trait Paint { 779 | fn fill(&self, context: &Context); 780 | fn stroke(&self, context: &Context); 781 | } 782 | 783 | /// A 32-bit color value. 784 | /// Used to fill or stroke paths with solid color. 785 | #[derive(Clone, Copy, Debug, PartialEq)] 786 | pub struct Color(ffi::NVGcolor); 787 | 788 | impl Color { 789 | /// Create a new color by setting all components manually. 790 | /// Values are in the range 0.0...1.0. 791 | pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self { 792 | Color(ffi::NVGcolor { rgba: [r, g, b, a] }) 793 | } 794 | 795 | /// Create a new color from three 8-bit color channels. 796 | pub fn from_rgb(r: u8, g: u8, b: u8) -> Self { 797 | Color(unsafe { ffi::nvgRGB(r as c_uchar, g as c_uchar, b as c_uchar) }) 798 | } 799 | 800 | /// Create a new color from three 8-bit color channels and an 8-bit alpha channel. 801 | pub fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Self { 802 | Color(unsafe { ffi::nvgRGBA(r as c_uchar, g as c_uchar, b as c_uchar, a as c_uchar) }) 803 | } 804 | 805 | /// Create a new color from three hsl channels. 806 | pub fn from_hsl(h: f32, s: f32, l: f32) -> Self { 807 | Color(unsafe { ffi::nvgHSL(h as c_float, s as c_float, l as c_float) }) 808 | } 809 | 810 | /// Create a new color from three hsl channels and an 8-bit alpha channel. 811 | pub fn from_hsla(h: f32, s: f32, l: f32, a: u8) -> Self { 812 | Color(unsafe { ffi::nvgHSLA(h as c_float, s as c_float, l as c_float, a as c_uchar) }) 813 | } 814 | 815 | fn into_raw(self) -> ffi::NVGcolor { 816 | self.0 817 | } 818 | 819 | /// Get the red component. 820 | pub fn red(&self) -> f32 { 821 | self.0.rgba[0] 822 | } 823 | 824 | /// Get the green component. 825 | pub fn green(&self) -> f32 { 826 | self.0.rgba[1] 827 | } 828 | 829 | /// Get the blue component. 830 | pub fn blue(&self) -> f32 { 831 | self.0.rgba[2] 832 | } 833 | 834 | /// Get the alpha component. 835 | pub fn alpha(&self) -> f32 { 836 | self.0.rgba[3] 837 | } 838 | 839 | /// Set the red component. 840 | pub fn set_red(&mut self, red: f32) { 841 | self.0.rgba[0] = red; 842 | } 843 | 844 | /// Get the green component. 845 | pub fn set_green(&mut self, green: f32) { 846 | self.0.rgba[1] = green; 847 | } 848 | 849 | /// Get the blue component. 850 | pub fn set_blue(&mut self, blue: f32) { 851 | self.0.rgba[2] = blue; 852 | } 853 | 854 | /// Get the alpha component. 855 | pub fn set_alpha(&mut self, alpha: f32) { 856 | self.0.rgba[3] = alpha; 857 | } 858 | 859 | /// Create a new color by linearly interpolating between two existing colors. 860 | pub fn lerp(a: Color, b: Color, t: f32) -> Color { 861 | Color(unsafe { ffi::nvgLerpRGBA(a.into_raw(), b.into_raw(), t as c_float) }) 862 | } 863 | } 864 | 865 | impl Paint for Color { 866 | fn fill(&self, context: &Context) { 867 | unsafe { 868 | ffi::nvgFillColor(context.raw(), self.into_raw()); 869 | } 870 | } 871 | 872 | fn stroke(&self, context: &Context) { 873 | unsafe { 874 | ffi::nvgStrokeColor(context.raw(), self.into_raw()); 875 | } 876 | } 877 | } 878 | 879 | impl Paint for Gradient { 880 | fn fill(&self, context: &Context) { 881 | let raw = self.create_raw(); 882 | unsafe { 883 | ffi::nvgFillPaint(context.raw(), raw); 884 | } 885 | } 886 | 887 | fn stroke(&self, context: &Context) { 888 | let raw = self.create_raw(); 889 | unsafe { 890 | ffi::nvgStrokePaint(context.raw(), raw); 891 | } 892 | } 893 | } 894 | 895 | /// Gradient paint used to fill or stroke paths with gradient. 896 | #[derive(Copy, Clone, Debug, PartialEq)] 897 | pub enum Gradient { 898 | Linear { 899 | start: (f32, f32), 900 | end: (f32, f32), 901 | start_color: Color, 902 | end_color: Color, 903 | }, 904 | Box { 905 | position: (f32, f32), 906 | size: (f32, f32), 907 | radius: f32, 908 | feather: f32, 909 | start_color: Color, 910 | end_color: Color, 911 | }, 912 | Radial { 913 | center: (f32, f32), 914 | inner_radius: f32, 915 | outer_radius: f32, 916 | start_color: Color, 917 | end_color: Color, 918 | }, 919 | } 920 | 921 | impl Gradient { 922 | fn create_raw(&self) -> ffi::NVGpaint { 923 | match self { 924 | &Gradient::Linear { 925 | start, 926 | end, 927 | start_color, 928 | end_color, 929 | } => { 930 | let (sx, sy) = start; 931 | let (ex, ey) = end; 932 | unsafe { 933 | ffi::nvgLinearGradient( 934 | ptr::null_mut(), 935 | sx, 936 | sy, 937 | ex, 938 | ey, 939 | start_color.into_raw(), 940 | end_color.into_raw(), 941 | ) 942 | } 943 | } 944 | &Gradient::Box { 945 | position, 946 | size, 947 | radius, 948 | feather, 949 | start_color, 950 | end_color, 951 | } => unsafe { 952 | let (x, y) = position; 953 | let (w, h) = size; 954 | ffi::nvgBoxGradient( 955 | ptr::null_mut(), 956 | x, 957 | y, 958 | w, 959 | h, 960 | radius, 961 | feather, 962 | start_color.into_raw(), 963 | end_color.into_raw(), 964 | ) 965 | }, 966 | &Gradient::Radial { 967 | center, 968 | inner_radius, 969 | outer_radius, 970 | start_color, 971 | end_color, 972 | } => unsafe { 973 | let (cx, cy) = center; 974 | ffi::nvgRadialGradient( 975 | ptr::null_mut(), 976 | cx, 977 | cy, 978 | inner_radius, 979 | outer_radius, 980 | start_color.into_raw(), 981 | end_color.into_raw(), 982 | ) 983 | }, 984 | } 985 | } 986 | } 987 | 988 | /// Image pattern paint used to fill or stroke paths with image pattern. 989 | #[derive(Copy, Clone, Debug)] 990 | pub struct ImagePattern<'a> { 991 | pub image: &'a Image<'a>, 992 | pub origin: (f32, f32), 993 | pub size: (f32, f32), 994 | pub angle: f32, 995 | pub alpha: f32, 996 | } 997 | 998 | impl<'a> ImagePattern<'a> { 999 | fn create_raw(&self) -> ffi::NVGpaint { 1000 | let (ox, oy) = self.origin; 1001 | let (ex, ey) = self.size; 1002 | unsafe { 1003 | ffi::nvgImagePattern( 1004 | ptr::null_mut(), 1005 | ox, 1006 | oy, 1007 | ex, 1008 | ey, 1009 | self.angle, 1010 | self.image.raw(), 1011 | self.alpha, 1012 | ) 1013 | } 1014 | } 1015 | } 1016 | 1017 | impl<'a> Paint for ImagePattern<'a> { 1018 | fn fill(&self, context: &Context) { 1019 | let raw = self.create_raw(); 1020 | unsafe { 1021 | ffi::nvgFillPaint(context.raw(), raw); 1022 | } 1023 | } 1024 | 1025 | fn stroke(&self, context: &Context) { 1026 | let raw = self.create_raw(); 1027 | unsafe { 1028 | ffi::nvgStrokePaint(context.raw(), raw); 1029 | } 1030 | } 1031 | } 1032 | 1033 | #[derive(Debug)] 1034 | pub struct ImageBuilder<'a> { 1035 | context: &'a Context, 1036 | flags: ffi::NVGimageFlags, 1037 | } 1038 | 1039 | impl<'a> ImageBuilder<'a> { 1040 | fn new(context: &'a Context) -> Self { 1041 | Self { 1042 | context, 1043 | flags: ffi::NVGimageFlags::empty(), 1044 | } 1045 | } 1046 | 1047 | /// Get the underlying context this ImageBuilder was created on. 1048 | pub fn context(&self) -> &'a Context { 1049 | self.context 1050 | } 1051 | 1052 | /// Create mipmaps during the creation of the image. 1053 | pub fn mipmaps(mut self) -> Self { 1054 | self.flags |= ffi::NVGimageFlags::NVG_IMAGE_GENERATE_MIPMAPS; 1055 | self 1056 | } 1057 | 1058 | /// Repeat the image on the X axis. 1059 | pub fn repeat_x(mut self) -> Self { 1060 | self.flags |= ffi::NVGimageFlags::NVG_IMAGE_REPEATX; 1061 | self 1062 | } 1063 | 1064 | /// Repeat the image on the Y axis. 1065 | pub fn repeat_y(mut self) -> Self { 1066 | self.flags |= ffi::NVGimageFlags::NVG_IMAGE_REPEATY; 1067 | self 1068 | } 1069 | 1070 | /// Flip (invert) the image in the Y direction during rendering. 1071 | pub fn flipy(mut self) -> Self { 1072 | self.flags |= ffi::NVGimageFlags::NVG_IMAGE_FLIPY; 1073 | self 1074 | } 1075 | 1076 | /// The image data contains premultiplied alpha. 1077 | pub fn premultiplied(mut self) -> Self { 1078 | self.flags |= ffi::NVGimageFlags::NVG_IMAGE_PREMULTIPLIED; 1079 | self 1080 | } 1081 | 1082 | /// Use nearest interpolation instead of linear. 1083 | pub fn nearest(mut self) -> Self { 1084 | self.flags |= ffi::NVGimageFlags::NVG_IMAGE_NEAREST; 1085 | self 1086 | } 1087 | 1088 | /// Construct the image by loading it from an image file on the file system. 1089 | pub fn build_from_file>(self, file: P) -> ImageBuilderResult<'a> { 1090 | let path = match file.as_ref().to_str() { 1091 | Some(p) => CString::new(p.to_owned())?, 1092 | None => return Err(ImageBuilderError::CStringError), 1093 | }; 1094 | 1095 | let handle = unsafe { ffi::nvgCreateImage(self.context.raw(), (*path).as_ptr(), self.flags.bits()) }; 1096 | if handle > 0 { 1097 | Ok(Image(self.context, handle)) 1098 | } else { 1099 | Err(ImageBuilderError::CreateImageFailed) 1100 | } 1101 | } 1102 | 1103 | /// Construct the image by loading it from an image file in memory. 1104 | pub fn build_from_memory(self, data: &[u8]) -> ImageBuilderResult<'a> { 1105 | let handle = unsafe { 1106 | ffi::nvgCreateImageMem( 1107 | self.context.raw(), 1108 | self.flags.bits(), 1109 | data.as_ptr() as *mut _, 1110 | data.len() as c_int, 1111 | ) 1112 | }; 1113 | if handle > 0 { 1114 | Ok(Image(self.context, handle)) 1115 | } else { 1116 | Err(ImageBuilderError::CreateImageFailed) 1117 | } 1118 | } 1119 | 1120 | /// Construct the image by filling it with pixel data from memory (always 32bit RGBA). 1121 | pub fn build_from_rgba(self, width: usize, height: usize, data: &[u32]) -> ImageBuilderResult<'a> { 1122 | if data.len() < width * height { 1123 | return Err(ImageBuilderError::NotEnoughData); 1124 | } 1125 | 1126 | let handle = unsafe { 1127 | ffi::nvgCreateImageRGBA( 1128 | self.context.raw(), 1129 | width as c_int, 1130 | height as c_int, 1131 | self.flags.bits(), 1132 | data.as_ptr() as *const _, 1133 | ) 1134 | }; 1135 | if handle > 0 { 1136 | Ok(Image(self.context, handle)) 1137 | } else { 1138 | Err(ImageBuilderError::CreateImageFailed) 1139 | } 1140 | } 1141 | } 1142 | 1143 | #[derive(Clone, Copy, Debug)] 1144 | pub enum ImageBuilderError { 1145 | /// The path for `build_from_file` could not be converted to a c-string. 1146 | CStringError, 1147 | /// The call to `nvgCreateImage`, or similar functions, failed. 1148 | CreateImageFailed, 1149 | /// For `from_rgba`, the passed data slice does not contain enough data for the specified image size. 1150 | NotEnoughData, 1151 | } 1152 | 1153 | impl From for ImageBuilderError { 1154 | fn from(_: NulError) -> Self { 1155 | ImageBuilderError::CStringError 1156 | } 1157 | } 1158 | 1159 | pub type ImageBuilderResult<'a> = Result, ImageBuilderError>; 1160 | 1161 | /// An owned image. 1162 | #[derive(Debug)] 1163 | pub struct Image<'a>(&'a Context, c_int); 1164 | 1165 | impl<'a> Image<'a> { 1166 | pub fn new(context: &'a Context) -> ImageBuilder { 1167 | ImageBuilder::new(context) 1168 | } 1169 | 1170 | /// Get the underlying context this image was created on. 1171 | pub fn context(&self) -> &'a Context { 1172 | self.0 1173 | } 1174 | 1175 | pub fn size(&self) -> (usize, usize) { 1176 | let (mut w, mut h): (c_int, c_int) = (0, 0); 1177 | unsafe { 1178 | ffi::nvgImageSize(self.ctx().raw(), self.raw(), &mut w as *mut _, &mut h as *mut _); 1179 | } 1180 | (w as usize, h as usize) 1181 | } 1182 | 1183 | pub fn update(&mut self, data: &[u32]) { 1184 | unsafe { 1185 | ffi::nvgUpdateImage(self.ctx().raw(), self.raw(), data.as_ptr() as *const _); 1186 | } 1187 | } 1188 | 1189 | fn ctx(&self) -> &Context { 1190 | self.0 1191 | } 1192 | 1193 | fn raw(&self) -> c_int { 1194 | self.1 1195 | } 1196 | } 1197 | 1198 | impl<'a> Drop for Image<'a> { 1199 | fn drop(&mut self) { 1200 | unsafe { 1201 | ffi::nvgDeleteImage(self.ctx().raw(), self.raw()); 1202 | } 1203 | self.1 = 0; 1204 | } 1205 | } 1206 | 1207 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 1208 | pub enum Direction { 1209 | Clockwise, 1210 | CounterClockwise, 1211 | } 1212 | 1213 | impl Direction { 1214 | fn into_raw(self) -> ffi::NVGwinding { 1215 | match self { 1216 | Direction::Clockwise => ffi::NVGwinding::NVG_CW, 1217 | Direction::CounterClockwise => ffi::NVGwinding::NVG_CCW, 1218 | } 1219 | } 1220 | } 1221 | 1222 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 1223 | pub enum Solidity { 1224 | Hole, 1225 | Solid, 1226 | } 1227 | 1228 | impl Solidity { 1229 | fn into_raw(self) -> ffi::NVGsolidity { 1230 | match self { 1231 | Solidity::Hole => ffi::NVGsolidity::NVG_HOLE, 1232 | Solidity::Solid => ffi::NVGsolidity::NVG_SOLID, 1233 | } 1234 | } 1235 | } 1236 | 1237 | /// Winding enum that holds either Direction or Solidity enum 1238 | /// These two are identical aliases. 1239 | /// They are here for different meanings in different contexts 1240 | #[derive(Debug, PartialEq, Eq)] 1241 | pub enum Winding { 1242 | Direction(Direction), 1243 | Solidity(Solidity), 1244 | } 1245 | 1246 | impl Winding { 1247 | fn into_raw(self) -> c_int { 1248 | match self { 1249 | Winding::Direction(direction) => direction.into_raw().bits(), 1250 | Winding::Solidity(solidity) => solidity.into_raw().bits(), 1251 | } 1252 | } 1253 | } 1254 | 1255 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 1256 | pub enum CompositeOperation { 1257 | Basic(BasicCompositeOperation), 1258 | BlendFunc { 1259 | source: BlendFactor, 1260 | destination: BlendFactor, 1261 | }, 1262 | BlendFuncSeparate { 1263 | rgb_source: BlendFactor, 1264 | rgb_destination: BlendFactor, 1265 | alpha_source: BlendFactor, 1266 | alpha_destination: BlendFactor, 1267 | }, 1268 | } 1269 | 1270 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 1271 | pub enum BasicCompositeOperation { 1272 | SourceOver, 1273 | SourceIn, 1274 | SourceOut, 1275 | Atop, 1276 | DestinationOver, 1277 | DestinationIn, 1278 | DestinationOut, 1279 | DestinationAtop, 1280 | Lighter, 1281 | Copy, 1282 | Xor, 1283 | } 1284 | 1285 | impl BasicCompositeOperation { 1286 | fn into_raw(self) -> ffi::NVGcompositeOperation { 1287 | use crate::ffi::NVGcompositeOperation::*; 1288 | use crate::BasicCompositeOperation::*; 1289 | match self { 1290 | SourceOver => NVG_SOURCE_OVER, 1291 | SourceIn => NVG_SOURCE_IN, 1292 | SourceOut => NVG_SOURCE_OUT, 1293 | Atop => NVG_ATOP, 1294 | DestinationOver => NVG_DESTINATION_OVER, 1295 | DestinationIn => NVG_DESTINATION_IN, 1296 | DestinationOut => NVG_DESTINATION_OUT, 1297 | DestinationAtop => NVG_DESTINATION_ATOP, 1298 | Lighter => NVG_LIGHTER, 1299 | Copy => NVG_COPY, 1300 | Xor => NVG_XOR, 1301 | } 1302 | } 1303 | } 1304 | 1305 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 1306 | pub enum BlendFactor { 1307 | Zero, 1308 | One, 1309 | SourceColor, 1310 | OneMinusSourceColor, 1311 | DestinationColor, 1312 | OneMinusDestinationColor, 1313 | SourceAlpha, 1314 | OneMinusSourceAlpha, 1315 | DestinationAlpha, 1316 | OneMinusDestinationAlpha, 1317 | SourceAlphaSaturate, 1318 | } 1319 | 1320 | impl BlendFactor { 1321 | fn into_raw(self) -> ffi::NVGblendFactor { 1322 | use crate::BlendFactor::*; 1323 | match self { 1324 | Zero => ffi::NVGblendFactor::NVG_ZERO, 1325 | One => ffi::NVGblendFactor::NVG_ONE, 1326 | SourceColor => ffi::NVGblendFactor::NVG_SRC_COLOR, 1327 | OneMinusSourceColor => ffi::NVGblendFactor::NVG_ONE_MINUS_SRC_COLOR, 1328 | DestinationColor => ffi::NVGblendFactor::NVG_DST_COLOR, 1329 | OneMinusDestinationColor => ffi::NVGblendFactor::NVG_ONE_MINUS_DST_COLOR, 1330 | SourceAlpha => ffi::NVGblendFactor::NVG_SRC_ALPHA, 1331 | OneMinusSourceAlpha => ffi::NVGblendFactor::NVG_ONE_MINUS_SRC_ALPHA, 1332 | DestinationAlpha => ffi::NVGblendFactor::NVG_DST_ALPHA, 1333 | OneMinusDestinationAlpha => ffi::NVGblendFactor::NVG_ONE_MINUS_DST_ALPHA, 1334 | SourceAlphaSaturate => ffi::NVGblendFactor::NVG_SRC_ALPHA_SATURATE, 1335 | } 1336 | } 1337 | } 1338 | 1339 | /// A handle to a font. 1340 | /// Fonts are managed by the NanoVG context itself. View this type only as a 'reference' to a font. 1341 | #[derive(Clone, Copy, Debug)] 1342 | pub struct Font<'a>(&'a Context, c_int); 1343 | 1344 | #[derive(Debug)] 1345 | pub enum CreateFontError { 1346 | /// Conversion from a Rust-utf8-string to a CString failed. 1347 | CStringError, 1348 | /// A specified path is invalid somehow. 1349 | InvalidPath, 1350 | /// The font handle returned by the ffi functions is invalid. 1351 | InvalidHandle, 1352 | } 1353 | 1354 | impl From for CreateFontError { 1355 | fn from(_: NulError) -> Self { 1356 | CreateFontError::CStringError 1357 | } 1358 | } 1359 | 1360 | pub type CreateFontResult<'a> = Result, CreateFontError>; 1361 | 1362 | impl<'a> Font<'a> { 1363 | fn ctx(&self) -> *mut ffi::NVGcontext { 1364 | self.0.raw() 1365 | } 1366 | 1367 | fn id(&self) -> c_int { 1368 | self.1 1369 | } 1370 | 1371 | /// Get the underlying context this font was created on. 1372 | pub fn context(&self) -> &'a Context { 1373 | self.0 1374 | } 1375 | 1376 | /// Attempt to load a font from the file at `path`. 1377 | /// Fonts are always named (specified with `name`). 1378 | pub fn from_file, P: AsRef>(context: &'a Context, name: S, path: P) -> CreateFontResult { 1379 | let name = CString::new(name.as_ref())?; 1380 | let path = CString::new(path.as_ref().to_str().ok_or(CreateFontError::InvalidPath)?)?; 1381 | let handle = unsafe { ffi::nvgCreateFont(context.raw(), name.as_ptr(), path.as_ptr()) }; 1382 | if handle > ffi::FONS_INVALID { 1383 | Ok(Font(context, handle)) 1384 | } else { 1385 | Err(CreateFontError::InvalidHandle) 1386 | } 1387 | } 1388 | 1389 | /// Attempt to load a font from memory. 1390 | /// Fonts are always named (specified with `name`). 1391 | pub fn from_memory<'b, S: AsRef>(context: &'a Context, name: S, memory: &'b [u8]) -> CreateFontResult<'a> { 1392 | let name = CString::new(name.as_ref())?; 1393 | let handle = unsafe { 1394 | ffi::nvgCreateFontMem( 1395 | context.raw(), 1396 | name.as_ptr(), 1397 | memory.as_ptr() as *mut _, 1398 | memory.len() as c_int, 1399 | 0, 1400 | ) 1401 | }; 1402 | if handle > ffi::FONS_INVALID { 1403 | Ok(Font(context, handle)) 1404 | } else { 1405 | Err(CreateFontError::InvalidHandle) 1406 | } 1407 | } 1408 | 1409 | /// Try to find a already loaded font with the given `name`. 1410 | pub fn find>(context: &'a Context, name: S) -> CreateFontResult { 1411 | let name = CString::new(name.as_ref())?; 1412 | let handle = unsafe { ffi::nvgFindFont(context.raw(), name.as_ptr()) }; 1413 | if handle > ffi::FONS_INVALID { 1414 | Ok(Font(context, handle)) 1415 | } else { 1416 | Err(CreateFontError::InvalidHandle) 1417 | } 1418 | } 1419 | 1420 | /// Add `fallback` as a fallback for the current font. 1421 | /// If the font renderer fails to rasterize a glyph with the main font, it will automatically 1422 | /// attempt to rasterize the same glyph with the fallback font. 1423 | /// This process continues until no working font is found, then the glyph is skipped. 1424 | pub fn add_fallback(&self, fallback: Font) -> bool { 1425 | let res = unsafe { ffi::nvgAddFallbackFontId(self.ctx(), self.id(), fallback.id()) }; 1426 | res != 0 1427 | } 1428 | } 1429 | 1430 | /// Options which control the visual appearance of a text. 1431 | #[derive(Clone, Copy, Debug, PartialEq)] 1432 | pub struct TextOptions { 1433 | /// The size of the text in points. 1434 | pub size: f32, 1435 | /// The radial blur of the text, in pixels. 1436 | pub blur: f32, 1437 | /// How much each individual letter of the text should be apart. 1438 | pub letter_spacing: f32, 1439 | /// The height for each line. Specified in multiplies of the font height. 1440 | /// Ex.: a `line_height` of 3.0 means each line is font height * 3 apart. 1441 | pub line_height: f32, 1442 | /// The width at which multiline text is automatically wrapped. 1443 | pub line_max_width: f32, 1444 | /// How to align the text. 1445 | pub align: Alignment, 1446 | /// The fill color of the text. 1447 | pub color: Color, 1448 | /// The clip defines the rectangular region in which the text is clipped into. 1449 | /// All overflowing pixels will be discarded. 1450 | pub clip: Clip, 1451 | /// A transformation which 'transforms' the coordinate system and consequently the text. 1452 | pub transform: Option, 1453 | } 1454 | 1455 | impl Default for TextOptions { 1456 | fn default() -> Self { 1457 | Self { 1458 | size: 12.0, 1459 | blur: 0.0, 1460 | letter_spacing: 0.0, 1461 | line_height: 1.0, 1462 | line_max_width: std::f32::MAX, 1463 | align: Alignment::new(), 1464 | color: Color::new(0.0, 0.0, 0.0, 0.0), 1465 | clip: Clip::None, 1466 | transform: None, 1467 | } 1468 | } 1469 | } 1470 | 1471 | /// Struct to store min and max bounds when measuring text with text_bounds or text_box_bounds 1472 | #[derive(Clone, Copy, Debug, PartialEq)] 1473 | pub struct TextBounds { 1474 | pub min_x: f32, 1475 | pub min_y: f32, 1476 | pub max_x: f32, 1477 | pub max_y: f32, 1478 | } 1479 | 1480 | impl TextBounds { 1481 | /// Creates new TextBounds struct instance from array 1482 | fn new(bounds: &[f32; 4]) -> TextBounds { 1483 | TextBounds { 1484 | min_x: bounds[0], 1485 | min_y: bounds[1], 1486 | max_x: bounds[2], 1487 | max_y: bounds[3], 1488 | } 1489 | } 1490 | } 1491 | 1492 | /// Iterator over text glyph positions, calculated by Context::text_glyph_positions 1493 | pub struct TextGlyphPositions<'a> { 1494 | context: &'a Context, 1495 | x: f32, 1496 | y: f32, 1497 | start: *const c_char, 1498 | glyphs: [ffi::NVGglyphPosition; 2], 1499 | } 1500 | 1501 | impl<'a> TextGlyphPositions<'a> { 1502 | /// Creates new TextGlyphPositions iterator with needed variables for iterating over glyphs in text 1503 | fn new(context: &'a Context, x: f32, y: f32, text: CString) -> TextGlyphPositions<'a> { 1504 | TextGlyphPositions { 1505 | context: context, 1506 | x: x, 1507 | y: y, 1508 | start: text.into_raw(), 1509 | glyphs: [unsafe { mem::zeroed() }; 2], 1510 | } 1511 | } 1512 | } 1513 | 1514 | impl<'a> Iterator for TextGlyphPositions<'a> { 1515 | type Item = GlyphPosition; 1516 | 1517 | /// Returns next glyph in text 1518 | fn next(&mut self) -> Option { 1519 | let num_glyphs = unsafe { 1520 | ffi::nvgTextGlyphPositions( 1521 | self.context.raw(), 1522 | self.x, 1523 | self.y, 1524 | self.start, 1525 | ptr::null(), 1526 | self.glyphs.as_mut_ptr(), 1527 | 2, 1528 | ) 1529 | }; 1530 | 1531 | match num_glyphs { 1532 | 1 => { 1533 | self.start = &('\0' as c_char); 1534 | Some(GlyphPosition::new(&self.glyphs[0])) 1535 | } 1536 | 2 => { 1537 | self.x = self.glyphs[1].x; 1538 | self.start = self.glyphs[1].s; 1539 | 1540 | Some(GlyphPosition::new(&self.glyphs[0])) 1541 | } 1542 | _ => None, 1543 | } 1544 | } 1545 | } 1546 | 1547 | /// Holds computed values for given row. 1548 | #[derive(Clone, Copy, Debug)] 1549 | pub struct TextRow<'a> { 1550 | pub width: f32, 1551 | pub min_x: f32, 1552 | pub max_x: f32, 1553 | pub text: &'a str, 1554 | } 1555 | 1556 | impl<'a> TextRow<'a> { 1557 | /// Creates new TextRow from raw nanovg text row 1558 | /// and also adds text contained in this row. 1559 | fn new(row: &ffi::NVGtextRow, text: &'a str) -> TextRow<'a> { 1560 | TextRow { 1561 | width: row.width, 1562 | min_x: row.minx, 1563 | max_x: row.maxx, 1564 | text: text, 1565 | } 1566 | } 1567 | } 1568 | 1569 | /// Iterator over rows in text 1570 | /// Returned by Context::text_break_lines 1571 | #[derive(Debug)] 1572 | pub struct TextBreakLines<'a> { 1573 | context: &'a Context, 1574 | start: *const c_char, 1575 | break_row_width: f32, 1576 | row: ffi::NVGtextRow, 1577 | } 1578 | 1579 | impl<'a> TextBreakLines<'a> { 1580 | /// Creates new TextBreakLines iterator which iterated over all text rows in text. 1581 | /// break_row_width specifies max length of row. 1582 | fn new(context: &'a Context, text: CString, break_row_width: f32) -> TextBreakLines<'a> { 1583 | TextBreakLines { 1584 | context: context, 1585 | start: text.into_raw(), 1586 | break_row_width: break_row_width, 1587 | row: unsafe { mem::zeroed() }, 1588 | } 1589 | } 1590 | } 1591 | 1592 | impl<'a> Iterator for TextBreakLines<'a> { 1593 | type Item = TextRow<'a>; 1594 | 1595 | /// Returns next row in text 1596 | fn next(&mut self) -> Option { 1597 | unsafe { 1598 | let nrows = ffi::nvgTextBreakLines( 1599 | self.context.raw(), 1600 | self.start, 1601 | ptr::null(), 1602 | self.break_row_width, 1603 | &mut self.row, 1604 | 1, 1605 | ); 1606 | self.start = self.row.next; 1607 | 1608 | if nrows > 0 { 1609 | let string_length = self.row.end as usize - self.row.start as usize; 1610 | let string_slice = std::slice::from_raw_parts(self.row.start as *const u8, string_length); 1611 | let text_str = std::str::from_utf8(string_slice).unwrap(); 1612 | Some(TextRow::new(&self.row, text_str)) 1613 | } else { 1614 | None 1615 | } 1616 | } 1617 | } 1618 | } 1619 | 1620 | // Stores position of glyph returned by iterator Context::text_glyph_positions 1621 | #[derive(Clone, Debug, PartialEq)] 1622 | pub struct GlyphPosition { 1623 | pub x: f32, 1624 | pub min_x: f32, 1625 | pub max_x: f32, 1626 | } 1627 | 1628 | impl GlyphPosition { 1629 | /// Creates new GlyphPosition from raw nanovg glyph position. 1630 | fn new(glyph: &ffi::NVGglyphPosition) -> GlyphPosition { 1631 | GlyphPosition { 1632 | x: glyph.x, 1633 | min_x: glyph.minx, 1634 | max_x: glyph.maxx, 1635 | } 1636 | } 1637 | } 1638 | 1639 | /// Struct to store measured text metrics computed with Context::text_metrics 1640 | #[derive(Clone, Copy, Debug, PartialEq)] 1641 | pub struct TextMetrics { 1642 | pub ascender: f32, 1643 | pub descender: f32, 1644 | pub line_height: f32, 1645 | } 1646 | 1647 | impl TextMetrics { 1648 | fn new() -> TextMetrics { 1649 | TextMetrics { 1650 | ascender: 0.0, 1651 | descender: 0.0, 1652 | line_height: 0.0, 1653 | } 1654 | } 1655 | } 1656 | 1657 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 1658 | pub struct Alignment(ffi::NVGalign); 1659 | 1660 | impl Alignment { 1661 | fn into_raw(self) -> ffi::NVGalign { 1662 | self.0 1663 | } 1664 | 1665 | /// Create a new top-left alignment. 1666 | pub fn new() -> Self { 1667 | Alignment(ffi::NVGalign::empty()).top().left() 1668 | } 1669 | 1670 | /// Set the horizontal alignment to left. 1671 | pub fn left(mut self) -> Self { 1672 | self.0.remove(ffi::NVGalign::NVG_ALIGN_RIGHT); 1673 | self.0.remove(ffi::NVGalign::NVG_ALIGN_CENTER); 1674 | self.0.insert(ffi::NVGalign::NVG_ALIGN_LEFT); 1675 | self 1676 | } 1677 | 1678 | /// Set the horizontal alignment to center. 1679 | pub fn center(mut self) -> Self { 1680 | self.0.remove(ffi::NVGalign::NVG_ALIGN_LEFT); 1681 | self.0.remove(ffi::NVGalign::NVG_ALIGN_RIGHT); 1682 | self.0.insert(ffi::NVGalign::NVG_ALIGN_CENTER); 1683 | self 1684 | } 1685 | 1686 | /// Set the horizontal alignment to right. 1687 | pub fn right(mut self) -> Self { 1688 | self.0.remove(ffi::NVGalign::NVG_ALIGN_LEFT); 1689 | self.0.remove(ffi::NVGalign::NVG_ALIGN_CENTER); 1690 | self.0.insert(ffi::NVGalign::NVG_ALIGN_RIGHT); 1691 | self 1692 | } 1693 | 1694 | /// Set the vertical alignment to top. 1695 | pub fn top(mut self) -> Self { 1696 | self.0.remove(ffi::NVGalign::NVG_ALIGN_BOTTOM); 1697 | self.0.remove(ffi::NVGalign::NVG_ALIGN_MIDDLE); 1698 | self.0.remove(ffi::NVGalign::NVG_ALIGN_BASELINE); 1699 | self.0.insert(ffi::NVGalign::NVG_ALIGN_TOP); 1700 | self 1701 | } 1702 | 1703 | /// Set the vertical alignment to middle. 1704 | pub fn middle(mut self) -> Self { 1705 | self.0.remove(ffi::NVGalign::NVG_ALIGN_TOP); 1706 | self.0.remove(ffi::NVGalign::NVG_ALIGN_BOTTOM); 1707 | self.0.remove(ffi::NVGalign::NVG_ALIGN_BASELINE); 1708 | self.0.insert(ffi::NVGalign::NVG_ALIGN_MIDDLE); 1709 | self 1710 | } 1711 | 1712 | /// Set the vertical alignment to bottom. 1713 | pub fn bottom(mut self) -> Self { 1714 | self.0.remove(ffi::NVGalign::NVG_ALIGN_TOP); 1715 | self.0.remove(ffi::NVGalign::NVG_ALIGN_MIDDLE); 1716 | self.0.remove(ffi::NVGalign::NVG_ALIGN_BASELINE); 1717 | self.0.insert(ffi::NVGalign::NVG_ALIGN_BOTTOM); 1718 | self 1719 | } 1720 | 1721 | /// Set the vertical alignment to baseline. 1722 | pub fn baseline(mut self) -> Self { 1723 | self.0.remove(ffi::NVGalign::NVG_ALIGN_TOP); 1724 | self.0.remove(ffi::NVGalign::NVG_ALIGN_MIDDLE); 1725 | self.0.remove(ffi::NVGalign::NVG_ALIGN_BOTTOM); 1726 | self.0.insert(ffi::NVGalign::NVG_ALIGN_BASELINE); 1727 | self 1728 | } 1729 | } 1730 | 1731 | /// Represents a transformation in 2D space. 1732 | /// 1733 | /// A transformation is a combination of translation (aka. position), skew and scale **or** 1734 | /// translation and rotation; implemented as a column-major matrix in the following form: 1735 | /// **[a c e]** - indices [0 2 4] 1736 | /// **[b d f]** - indices [1 3 5] 1737 | /// **[0 0 1]** - only theoretical / does not really exist. Logically it is always [0 0 1]. 1738 | #[derive(Clone, Copy, Debug, PartialEq)] 1739 | pub struct Transform { 1740 | pub matrix: [f32; 6], 1741 | /// Controls whether paths or texts that gets transformed by this Transform 1742 | /// are drawn in absolute coordinate space or coordinate space relative to the one 1743 | /// previously active (relative positioning is default) 1744 | /// This is just flag to tell drawing functions to use this Transform for drawing, 1745 | /// it does not modify the underlying matrix. 1746 | absolute: bool, 1747 | } 1748 | 1749 | impl Transform { 1750 | /// Construct a new transform with an identity matrix. 1751 | pub fn new() -> Self { 1752 | Self { 1753 | matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], 1754 | absolute: false, 1755 | } 1756 | } 1757 | 1758 | /// Set flag on this transform to use it in absolute coordinate space. Only applies to text. 1759 | pub fn absolute(mut self) -> Self { 1760 | self.absolute = true; 1761 | self 1762 | } 1763 | 1764 | /// Set flag on this transform to use it in local (relative) coordinate space. Only applies to text. 1765 | pub fn relative(mut self) -> Self { 1766 | self.absolute = false; 1767 | self 1768 | } 1769 | 1770 | /// Set the translation of the transform. 1771 | pub fn with_translation(self, x: f32, y: f32) -> Self { 1772 | let mut new = self.clone(); 1773 | new.matrix[4] = x; 1774 | new.matrix[5] = y; 1775 | new 1776 | } 1777 | 1778 | /// Set the scale of the transform. 1779 | pub fn with_scale(self, x: f32, y: f32) -> Self { 1780 | let mut new = self.clone(); 1781 | new.matrix[0] = x; 1782 | new.matrix[3] = y; 1783 | new 1784 | } 1785 | 1786 | /// Set the skew of the transform. 1787 | pub fn with_skew(self, x: f32, y: f32) -> Self { 1788 | let mut new = self.clone(); 1789 | new.matrix[2] = x; 1790 | new.matrix[1] = y; 1791 | new 1792 | } 1793 | 1794 | /// Set the rotation of the transform. 1795 | pub fn with_rotation(self, theta: f32) -> Self { 1796 | let mut new = self.clone(); 1797 | new.matrix[0] = theta.cos(); 1798 | new.matrix[2] = -theta.sin(); 1799 | new.matrix[1] = theta.sin(); 1800 | new.matrix[3] = theta.cos(); 1801 | new 1802 | } 1803 | 1804 | /// Translate transform by x and y. 1805 | pub fn translate(self, x: f32, y: f32) -> Self { 1806 | let mut new = self.clone(); 1807 | let mut t = [0.0f32; 6]; 1808 | unsafe { 1809 | ffi::nvgTransformTranslate(t.as_mut_ptr(), x, y); 1810 | ffi::nvgTransformPremultiply(new.matrix.as_mut_ptr(), t.as_mut_ptr()); 1811 | } 1812 | new 1813 | } 1814 | 1815 | /// Rotate transform with spcified angle. 1816 | pub fn rotate(self, angle: f32) -> Self { 1817 | let mut new = self.clone(); 1818 | let mut t = [0.0f32; 6]; 1819 | unsafe { 1820 | ffi::nvgTransformRotate(t.as_mut_ptr(), angle); 1821 | ffi::nvgTransformPremultiply(new.matrix.as_mut_ptr(), t.as_mut_ptr()); 1822 | } 1823 | new 1824 | } 1825 | 1826 | /// Skew transform along x axis with specified angle. 1827 | pub fn skew_x(self, angle: f32) -> Self { 1828 | let mut new = self.clone(); 1829 | let mut t = [0.0f32; 6]; 1830 | unsafe { 1831 | ffi::nvgTransformSkewX(t.as_mut_ptr(), angle); 1832 | ffi::nvgTransformPremultiply(new.matrix.as_mut_ptr(), t.as_mut_ptr()); 1833 | } 1834 | new 1835 | } 1836 | 1837 | /// Skew transform along y axis with specified angle. 1838 | pub fn skew_y(self, angle: f32) -> Self { 1839 | let mut new = self.clone(); 1840 | let mut t = [0.0f32; 6]; 1841 | unsafe { 1842 | ffi::nvgTransformSkewY(t.as_mut_ptr(), angle); 1843 | ffi::nvgTransformPremultiply(new.matrix.as_mut_ptr(), t.as_mut_ptr()); 1844 | } 1845 | new 1846 | } 1847 | 1848 | /// Scale transform along x and y. 1849 | pub fn scale(self, x: f32, y: f32) -> Self { 1850 | let mut new = self.clone(); 1851 | let mut t = [0.0f32; 6]; 1852 | unsafe { 1853 | ffi::nvgTransformScale(t.as_mut_ptr(), x, y); 1854 | ffi::nvgTransformPremultiply(new.matrix.as_mut_ptr(), t.as_mut_ptr()); 1855 | } 1856 | new 1857 | } 1858 | 1859 | /// Transforms a point with this transform. 1860 | /// Returns transformed point (x, y). 1861 | pub fn transform_point(&self, (x, y): (f32, f32)) -> (f32, f32) { 1862 | let mut transformed = (0.0f32, 0.0f32); 1863 | unsafe { 1864 | ffi::nvgTransformPoint(&mut transformed.0, &mut transformed.1, self.matrix.as_ptr(), x, y); 1865 | } 1866 | transformed 1867 | } 1868 | 1869 | /// Inverses this transform. 1870 | /// Returns inversed copy or None if inversion fails. 1871 | pub fn try_inverse(&self) -> Option { 1872 | let mut inv = Transform::new(); 1873 | let result = unsafe { ffi::nvgTransformInverse(inv.matrix.as_mut_ptr(), self.matrix.as_ptr()) }; 1874 | 1875 | if result == 1 { 1876 | Some(inv) 1877 | } else { 1878 | None 1879 | } 1880 | } 1881 | } 1882 | 1883 | /// Implementation of multiplication Trait for Transform. 1884 | /// The order in which you multiplicate matters (you are multiplicating matrices) 1885 | impl std::ops::Mul for Transform { 1886 | type Output = Transform; 1887 | 1888 | /// Multiplies transform with other transform (the order matters). 1889 | fn mul(self, rhs: Transform) -> Self::Output { 1890 | let mut result = self.clone(); 1891 | unsafe { 1892 | ffi::nvgTransformMultiply(result.matrix.as_mut_ptr(), rhs.matrix.as_ptr()); 1893 | } 1894 | result 1895 | } 1896 | } 1897 | 1898 | #[cfg(test)] 1899 | mod tests { 1900 | use super::*; 1901 | 1902 | macro_rules! trans_eq_bool { 1903 | ($t1:expr, $t2:expr) => { 1904 | ($t1.matrix[0] - $t2.matrix[0]).abs() < 0.01 1905 | && ($t1.matrix[1] - $t2.matrix[1]).abs() < 0.01 1906 | && ($t1.matrix[2] - $t2.matrix[2]).abs() < 0.01 1907 | && ($t1.matrix[3] - $t2.matrix[3]).abs() < 0.01 1908 | && ($t1.matrix[4] - $t2.matrix[4]).abs() < 0.01 1909 | && ($t1.matrix[5] - $t2.matrix[5]).abs() < 0.01 1910 | }; 1911 | } 1912 | 1913 | macro_rules! trans_eq { 1914 | ($t1:expr, $t2:expr) => { 1915 | assert!(trans_eq_bool!($t1, $t2)) 1916 | }; 1917 | } 1918 | 1919 | macro_rules! trans_not_eq { 1920 | ($t1:expr, $t2:expr) => { 1921 | assert!(!trans_eq_bool!($t1, $t2)) 1922 | }; 1923 | } 1924 | 1925 | #[test] 1926 | fn test_transform() { 1927 | // Contructors 1928 | trans_eq!( 1929 | Transform::new(), 1930 | Transform { 1931 | matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], 1932 | absolute: false, 1933 | } 1934 | ); 1935 | 1936 | trans_eq!( 1937 | Transform::new().with_translation(11.1, 22.2), 1938 | Transform { 1939 | matrix: [1.0, 0.0, 0.0, 1.0, 11.1, 22.2], 1940 | absolute: false, 1941 | } 1942 | ); 1943 | 1944 | trans_eq!( 1945 | Transform::new().with_scale(11.1, 22.2), 1946 | Transform { 1947 | matrix: [11.1, 0.0, 0.0, 22.2, 0.0, 0.0], 1948 | absolute: false, 1949 | } 1950 | ); 1951 | 1952 | trans_eq!( 1953 | Transform::new().with_skew(11.1, 22.2), 1954 | Transform { 1955 | matrix: [1.0, 22.2, 11.1, 1.0, 0.0, 0.0], 1956 | absolute: false, 1957 | } 1958 | ); 1959 | 1960 | let angle = 90f32.to_radians(); 1961 | trans_eq!( 1962 | Transform::new().with_rotation(angle), 1963 | Transform { 1964 | matrix: [angle.cos(), angle.sin(), -angle.sin(), angle.cos(), 0.0, 0.0], 1965 | absolute: false, 1966 | } 1967 | ); 1968 | 1969 | // Multiplication 1970 | let identity = Transform::new(); 1971 | let trans = Transform::new().with_translation(10.0, 20.0); 1972 | trans_eq!(identity * trans, trans); 1973 | trans_eq!(trans * identity, trans); 1974 | trans_eq!(identity * identity, identity); 1975 | let a = Transform::new().with_rotation(123.0); 1976 | let b = Transform::new().with_skew(66.6, 1337.2); 1977 | trans_not_eq!(a * b, b * a); 1978 | } 1979 | } 1980 | --------------------------------------------------------------------------------