diff options
Diffstat (limited to '')
-rw-r--r-- | glow/Cargo.toml | 36 | ||||
-rw-r--r-- | glow/src/backend.rs | 216 | ||||
-rw-r--r-- | glow/src/lib.rs | 39 | ||||
-rw-r--r-- | glow/src/program.rs | 39 | ||||
-rw-r--r-- | glow/src/quad.rs | 235 | ||||
-rw-r--r-- | glow/src/settings.rs | 25 | ||||
-rw-r--r-- | glow/src/shader/quad.frag | 70 | ||||
-rw-r--r-- | glow/src/shader/quad.vert | 47 | ||||
-rw-r--r-- | glow/src/shader/triangle.frag | 9 | ||||
-rw-r--r-- | glow/src/shader/triangle.vert | 13 | ||||
-rw-r--r-- | glow/src/text.rs | 151 | ||||
-rw-r--r-- | glow/src/triangle.rs | 292 | ||||
-rw-r--r-- | glow/src/widget.rs | 58 | ||||
-rw-r--r-- | glow/src/widget/button.rs | 15 | ||||
-rw-r--r-- | glow/src/widget/canvas.rs | 9 | ||||
-rw-r--r-- | glow/src/widget/checkbox.rs | 9 | ||||
-rw-r--r-- | glow/src/widget/container.rs | 10 | ||||
-rw-r--r-- | glow/src/widget/pane_grid.rs | 24 | ||||
-rw-r--r-- | glow/src/widget/progress_bar.rs | 15 | ||||
-rw-r--r-- | glow/src/widget/radio.rs | 10 | ||||
-rw-r--r-- | glow/src/widget/scrollable.rs | 13 | ||||
-rw-r--r-- | glow/src/widget/slider.rs | 16 | ||||
-rw-r--r-- | glow/src/widget/text_input.rs | 15 | ||||
-rw-r--r-- | glow/src/window.rs | 4 | ||||
-rw-r--r-- | glow/src/window/compositor.rs | 74 |
25 files changed, 1444 insertions, 0 deletions
diff --git a/glow/Cargo.toml b/glow/Cargo.toml new file mode 100644 index 00000000..262f0264 --- /dev/null +++ b/glow/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "iced_glow" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +description = "A glow renderer for iced" +license = "MIT AND OFL-1.1" +repository = "https://github.com/hecrj/iced" + +[features] +canvas = ["iced_graphics/canvas"] +# Not supported yet! +image = [] +svg = [] + +[dependencies] +glow = "0.4" +glow_glyph = "0.2" +glyph_brush = "0.7" +euclid = "0.20" +bytemuck = "1.2" +glam = "0.8" +log = "0.4" + +[dependencies.iced_native] +version = "0.2" +path = "../native" + +[dependencies.iced_graphics] +version = "0.1" +path = "../graphics" +features = ["font-source", "font-fallback", "font-icons", "opengl"] + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true diff --git a/glow/src/backend.rs b/glow/src/backend.rs new file mode 100644 index 00000000..6bd443ad --- /dev/null +++ b/glow/src/backend.rs @@ -0,0 +1,216 @@ +use crate::quad; +use crate::text; +use crate::triangle; +use crate::{Settings, Transformation, Viewport}; +use iced_graphics::backend; +use iced_graphics::font; +use iced_graphics::Layer; +use iced_graphics::Primitive; +use iced_native::mouse; +use iced_native::{Font, HorizontalAlignment, Size, VerticalAlignment}; + +/// A [`glow`] renderer. +/// +/// [`glow`]: https://github.com/grovesNL/glow +#[derive(Debug)] +pub struct Backend { + quad_pipeline: quad::Pipeline, + text_pipeline: text::Pipeline, + triangle_pipeline: triangle::Pipeline, +} + +impl Backend { + /// Creates a new [`Renderer`]. + /// + /// [`Renderer`]: struct.Renderer.html + pub fn new(gl: &glow::Context, settings: Settings) -> Self { + let text_pipeline = text::Pipeline::new(gl, settings.default_font); + let quad_pipeline = quad::Pipeline::new(gl); + let triangle_pipeline = triangle::Pipeline::new(gl); + + Self { + quad_pipeline, + text_pipeline, + triangle_pipeline, + } + } + + pub fn draw<T: AsRef<str>>( + &mut self, + gl: &glow::Context, + viewport: &Viewport, + (primitive, mouse_interaction): &(Primitive, mouse::Interaction), + overlay_text: &[T], + ) -> mouse::Interaction { + let viewport_size = viewport.physical_size(); + let scale_factor = viewport.scale_factor() as f32; + let projection = viewport.projection(); + + let mut layers = Layer::generate(primitive, viewport); + layers.push(Layer::overlay(overlay_text, viewport)); + + for layer in layers { + self.flush( + gl, + scale_factor, + projection, + &layer, + viewport_size.height, + ); + } + + *mouse_interaction + } + + fn flush( + &mut self, + gl: &glow::Context, + scale_factor: f32, + transformation: Transformation, + layer: &Layer<'_>, + target_height: u32, + ) { + let mut bounds = (layer.bounds * scale_factor).round(); + bounds.height = bounds.height.min(target_height); + + if !layer.quads.is_empty() { + self.quad_pipeline.draw( + gl, + target_height, + &layer.quads, + transformation, + scale_factor, + bounds, + ); + } + + if !layer.meshes.is_empty() { + let scaled = transformation + * Transformation::scale(scale_factor, scale_factor); + + self.triangle_pipeline.draw( + gl, + target_height, + scaled, + scale_factor, + &layer.meshes, + ); + } + + if !layer.text.is_empty() { + for text in layer.text.iter() { + // Target physical coordinates directly to avoid blurry text + let text = glow_glyph::Section { + // TODO: We `round` here to avoid rerasterizing text when + // its position changes slightly. This can make text feel a + // bit "jumpy". We may be able to do better once we improve + // our text rendering/caching pipeline. + screen_position: ( + (text.bounds.x * scale_factor).round(), + (text.bounds.y * scale_factor).round(), + ), + // TODO: Fix precision issues with some scale factors. + // + // The `ceil` here can cause some words to render on the + // same line when they should not. + // + // Ideally, `wgpu_glyph` should be able to compute layout + // using logical positions, and then apply the proper + // scaling when rendering. This would ensure that both + // measuring and rendering follow the same layout rules. + bounds: ( + (text.bounds.width * scale_factor).ceil(), + (text.bounds.height * scale_factor).ceil(), + ), + text: vec![glow_glyph::Text { + text: text.content, + scale: glow_glyph::ab_glyph::PxScale { + x: text.size * scale_factor, + y: text.size * scale_factor, + }, + font_id: self.text_pipeline.find_font(text.font), + extra: glow_glyph::Extra { + color: text.color, + z: 0.0, + }, + }], + layout: glow_glyph::Layout::default() + .h_align(match text.horizontal_alignment { + HorizontalAlignment::Left => { + glow_glyph::HorizontalAlign::Left + } + HorizontalAlignment::Center => { + glow_glyph::HorizontalAlign::Center + } + HorizontalAlignment::Right => { + glow_glyph::HorizontalAlign::Right + } + }) + .v_align(match text.vertical_alignment { + VerticalAlignment::Top => { + glow_glyph::VerticalAlign::Top + } + VerticalAlignment::Center => { + glow_glyph::VerticalAlign::Center + } + VerticalAlignment::Bottom => { + glow_glyph::VerticalAlign::Bottom + } + }), + ..Default::default() + }; + + self.text_pipeline.queue(text); + } + + self.text_pipeline.draw_queued( + gl, + transformation, + glow_glyph::Region { + x: bounds.x, + y: target_height - (bounds.y + bounds.height), + width: bounds.width, + height: bounds.height, + }, + ); + } + } +} + +impl iced_graphics::Backend for Backend { + fn trim_measurements(&mut self) { + self.text_pipeline.trim_measurement_cache() + } +} + +impl backend::Text for Backend { + const ICON_FONT: Font = font::ICONS; + const CHECKMARK_ICON: char = font::CHECKMARK_ICON; + + fn measure( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + ) -> (f32, f32) { + self.text_pipeline.measure(contents, size, font, bounds) + } +} + +#[cfg(feature = "image")] +impl backend::Image for Backend { + fn dimensions(&self, _handle: &iced_native::image::Handle) -> (u32, u32) { + (50, 50) + } +} + +#[cfg(feature = "svg")] +impl backend::Svg for Backend { + fn viewport_dimensions( + &self, + _handle: &iced_native::svg::Handle, + ) -> (u32, u32) { + (50, 50) + } +} diff --git a/glow/src/lib.rs b/glow/src/lib.rs new file mode 100644 index 00000000..c427d13a --- /dev/null +++ b/glow/src/lib.rs @@ -0,0 +1,39 @@ +//! A [`glow`] renderer for [`iced_native`]. +//! +//! [`glow`]: https://github.com/grovesNL/glow +//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(unused_results)] +#![forbid(rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +mod backend; +mod program; +mod quad; +mod text; +mod triangle; + +pub mod settings; +pub mod widget; +pub mod window; + +pub use settings::Settings; + +pub(crate) use backend::Backend; +pub(crate) use iced_graphics::Transformation; + +#[doc(no_inline)] +pub use widget::*; + +pub use iced_graphics::Viewport; +pub use iced_native::{ + Background, Color, Command, HorizontalAlignment, Length, Vector, + VerticalAlignment, +}; + +/// A [`glow`] graphics renderer for [`iced`]. +/// +/// [`glow`]: https://github.com/grovesNL/glow +/// [`iced`]: https://github.com/hecrj/iced +pub type Renderer = iced_graphics::Renderer<Backend>; diff --git a/glow/src/program.rs b/glow/src/program.rs new file mode 100644 index 00000000..489a194f --- /dev/null +++ b/glow/src/program.rs @@ -0,0 +1,39 @@ +use glow::HasContext; + +pub unsafe fn create( + gl: &glow::Context, + shader_sources: &[(u32, &str)], +) -> <glow::Context as HasContext>::Program { + let program = gl.create_program().expect("Cannot create program"); + + let mut shaders = Vec::with_capacity(shader_sources.len()); + + for (shader_type, shader_source) in shader_sources.iter() { + let shader = gl + .create_shader(*shader_type) + .expect("Cannot create shader"); + + gl.shader_source(shader, shader_source); + gl.compile_shader(shader); + + if !gl.get_shader_compile_status(shader) { + panic!(gl.get_shader_info_log(shader)); + } + + gl.attach_shader(program, shader); + + shaders.push(shader); + } + + gl.link_program(program); + if !gl.get_program_link_status(program) { + panic!(gl.get_program_info_log(program)); + } + + for shader in shaders { + gl.detach_shader(program, shader); + gl.delete_shader(shader); + } + + program +} diff --git a/glow/src/quad.rs b/glow/src/quad.rs new file mode 100644 index 00000000..3a65338a --- /dev/null +++ b/glow/src/quad.rs @@ -0,0 +1,235 @@ +use crate::program; +use crate::Transformation; +use glow::HasContext; +use iced_graphics::layer; +use iced_native::Rectangle; + +const MAX_INSTANCES: usize = 100_000; + +#[derive(Debug)] +pub struct Pipeline { + program: <glow::Context as HasContext>::Program, + vertex_array: <glow::Context as HasContext>::VertexArray, + instances: <glow::Context as HasContext>::Buffer, + transform_location: <glow::Context as HasContext>::UniformLocation, + scale_location: <glow::Context as HasContext>::UniformLocation, + screen_height_location: <glow::Context as HasContext>::UniformLocation, + current_transform: Transformation, + current_scale: f32, + current_target_height: u32, +} + +impl Pipeline { + pub fn new(gl: &glow::Context) -> Pipeline { + let program = unsafe { + program::create( + gl, + &[ + (glow::VERTEX_SHADER, include_str!("shader/quad.vert")), + (glow::FRAGMENT_SHADER, include_str!("shader/quad.frag")), + ], + ) + }; + + let transform_location = + unsafe { gl.get_uniform_location(program, "u_Transform") } + .expect("Get transform location"); + + let scale_location = + unsafe { gl.get_uniform_location(program, "u_Scale") } + .expect("Get scale location"); + + let screen_height_location = + unsafe { gl.get_uniform_location(program, "u_ScreenHeight") } + .expect("Get target height location"); + + unsafe { + gl.use_program(Some(program)); + + let matrix: [f32; 16] = Transformation::identity().into(); + gl.uniform_matrix_4_f32_slice( + Some(transform_location), + false, + &matrix, + ); + + gl.uniform_1_f32(Some(scale_location), 1.0); + gl.uniform_1_f32(Some(screen_height_location), 0.0); + + gl.use_program(None); + } + + let (vertex_array, instances) = + unsafe { create_instance_buffer(gl, MAX_INSTANCES) }; + + Pipeline { + program, + vertex_array, + instances, + transform_location, + scale_location, + screen_height_location, + current_transform: Transformation::identity(), + current_scale: 1.0, + current_target_height: 0, + } + } + + pub fn draw( + &mut self, + gl: &glow::Context, + target_height: u32, + instances: &[layer::Quad], + transformation: Transformation, + scale: f32, + bounds: Rectangle<u32>, + ) { + unsafe { + gl.enable(glow::SCISSOR_TEST); + gl.scissor( + bounds.x as i32, + (target_height - (bounds.y + bounds.height)) as i32, + bounds.width as i32, + bounds.height as i32, + ); + + gl.use_program(Some(self.program)); + gl.bind_vertex_array(Some(self.vertex_array)); + gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.instances)); + } + + if transformation != self.current_transform { + unsafe { + let matrix: [f32; 16] = transformation.into(); + gl.uniform_matrix_4_f32_slice( + Some(self.transform_location), + false, + &matrix, + ); + + self.current_transform = transformation; + } + } + + if scale != self.current_scale { + unsafe { + gl.uniform_1_f32(Some(self.scale_location), scale); + } + + self.current_scale = scale; + } + + if target_height != self.current_target_height { + unsafe { + gl.uniform_1_f32( + Some(self.screen_height_location), + target_height as f32, + ); + } + + self.current_target_height = target_height; + } + + let mut i = 0; + let total = instances.len(); + + while i < total { + let end = (i + MAX_INSTANCES).min(total); + let amount = end - i; + + unsafe { + gl.buffer_sub_data_u8_slice( + glow::ARRAY_BUFFER, + 0, + bytemuck::cast_slice(&instances[i..end]), + ); + + gl.draw_arrays_instanced( + glow::TRIANGLE_STRIP, + 0, + 4, + amount as i32, + ); + } + + i += MAX_INSTANCES; + } + + unsafe { + gl.bind_vertex_array(None); + gl.use_program(None); + gl.disable(glow::SCISSOR_TEST); + } + } +} + +unsafe fn create_instance_buffer( + gl: &glow::Context, + size: usize, +) -> ( + <glow::Context as HasContext>::VertexArray, + <glow::Context as HasContext>::Buffer, +) { + let vertex_array = gl.create_vertex_array().expect("Create vertex array"); + let buffer = gl.create_buffer().expect("Create instance buffer"); + + gl.bind_vertex_array(Some(vertex_array)); + gl.bind_buffer(glow::ARRAY_BUFFER, Some(buffer)); + gl.buffer_data_size( + glow::ARRAY_BUFFER, + (size * std::mem::size_of::<layer::Quad>()) as i32, + glow::DYNAMIC_DRAW, + ); + + let stride = std::mem::size_of::<layer::Quad>() as i32; + + gl.enable_vertex_attrib_array(0); + gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, stride, 0); + gl.vertex_attrib_divisor(0, 1); + + gl.enable_vertex_attrib_array(1); + gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, stride, 4 * 2); + gl.vertex_attrib_divisor(1, 1); + + gl.enable_vertex_attrib_array(2); + gl.vertex_attrib_pointer_f32(2, 4, glow::FLOAT, false, stride, 4 * (2 + 2)); + gl.vertex_attrib_divisor(2, 1); + + gl.enable_vertex_attrib_array(3); + gl.vertex_attrib_pointer_f32( + 3, + 4, + glow::FLOAT, + false, + stride, + 4 * (2 + 2 + 4), + ); + gl.vertex_attrib_divisor(3, 1); + + gl.enable_vertex_attrib_array(4); + gl.vertex_attrib_pointer_f32( + 4, + 1, + glow::FLOAT, + false, + stride, + 4 * (2 + 2 + 4 + 4), + ); + gl.vertex_attrib_divisor(4, 1); + + gl.enable_vertex_attrib_array(5); + gl.vertex_attrib_pointer_f32( + 5, + 1, + glow::FLOAT, + false, + stride, + 4 * (2 + 2 + 4 + 4 + 1), + ); + gl.vertex_attrib_divisor(5, 1); + + gl.bind_vertex_array(None); + gl.bind_buffer(glow::ARRAY_BUFFER, None); + + (vertex_array, buffer) +} diff --git a/glow/src/settings.rs b/glow/src/settings.rs new file mode 100644 index 00000000..dce30029 --- /dev/null +++ b/glow/src/settings.rs @@ -0,0 +1,25 @@ +//! Configure a renderer. +pub use iced_graphics::Antialiasing; + +/// The settings of a [`Renderer`]. +/// +/// [`Renderer`]: ../struct.Renderer.html +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Settings { + /// The bytes of the font that will be used by default. + /// + /// If `None` is provided, a default system font will be chosen. + pub default_font: Option<&'static [u8]>, + + /// The antialiasing strategy that will be used for triangle primitives. + pub antialiasing: Option<Antialiasing>, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + default_font: None, + antialiasing: None, + } + } +} diff --git a/glow/src/shader/quad.frag b/glow/src/shader/quad.frag new file mode 100644 index 00000000..cea36bdc --- /dev/null +++ b/glow/src/shader/quad.frag @@ -0,0 +1,70 @@ +#version 330 + +uniform float u_ScreenHeight; + +in vec4 v_Color; +in vec4 v_BorderColor; +in vec2 v_Pos; +in vec2 v_Scale; +in float v_BorderRadius; +in float v_BorderWidth; + +out vec4 o_Color; + +float distance(in vec2 frag_coord, in vec2 position, in vec2 size, float radius) +{ + // TODO: Try SDF approach: https://www.shadertoy.com/view/wd3XRN + vec2 inner_size = size - vec2(radius, radius) * 2.0; + vec2 top_left = position + vec2(radius, radius); + vec2 bottom_right = top_left + inner_size; + + vec2 top_left_distance = top_left - frag_coord; + vec2 bottom_right_distance = frag_coord - bottom_right; + + vec2 distance = vec2( + max(max(top_left_distance.x, bottom_right_distance.x), 0.0), + max(max(top_left_distance.y, bottom_right_distance.y), 0.0) + ); + + return sqrt(distance.x * distance.x + distance.y * distance.y); +} + +void main() { + vec4 mixed_color; + + vec2 fragCoord = vec2(gl_FragCoord.x, u_ScreenHeight - gl_FragCoord.y); + + // TODO: Remove branching (?) + if(v_BorderWidth > 0) { + float internal_border = max(v_BorderRadius - v_BorderWidth, 0.0); + + float internal_distance = distance( + fragCoord, + v_Pos + vec2(v_BorderWidth), + v_Scale - vec2(v_BorderWidth * 2.0), + internal_border + ); + + float border_mix = smoothstep( + max(internal_border - 0.5, 0.0), + internal_border + 0.5, + internal_distance + ); + + mixed_color = mix(v_Color, v_BorderColor, border_mix); + } else { + mixed_color = v_Color; + } + + float d = distance( + fragCoord, + v_Pos, + v_Scale, + v_BorderRadius + ); + + float radius_alpha = + 1.0 - smoothstep(max(v_BorderRadius - 0.5, 0.0), v_BorderRadius + 0.5, d); + + o_Color = vec4(mixed_color.xyz, mixed_color.w * radius_alpha); +} diff --git a/glow/src/shader/quad.vert b/glow/src/shader/quad.vert new file mode 100644 index 00000000..ce816550 --- /dev/null +++ b/glow/src/shader/quad.vert @@ -0,0 +1,47 @@ +#version 330 + +uniform mat4 u_Transform; +uniform float u_Scale; + +layout(location = 0) in vec2 i_Pos; +layout(location = 1) in vec2 i_Scale; +layout(location = 2) in vec4 i_Color; +layout(location = 3) in vec4 i_BorderColor; +layout(location = 4) in float i_BorderRadius; +layout(location = 5) in float i_BorderWidth; + +out vec4 v_Color; +out vec4 v_BorderColor; +out vec2 v_Pos; +out vec2 v_Scale; +out float v_BorderRadius; +out float v_BorderWidth; + +const vec2 positions[4] = vec2[]( + vec2(0.0, 0.0), + vec2(0.0, 1.0), + vec2(1.0, 0.0), + vec2(1.0, 1.0) +); + +void main() { + vec2 q_Pos = positions[gl_VertexID]; + vec2 p_Pos = floor(i_Pos * u_Scale); + vec2 p_Scale = floor(i_Scale * u_Scale); + + mat4 i_Transform = mat4( + vec4(p_Scale.x, 0.0, 0.0, 0.0), + vec4(0.0, p_Scale.y, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(p_Pos, 0.0, 1.0) + ); + + v_Color = i_Color; + v_BorderColor = i_BorderColor; + v_Pos = p_Pos; + v_Scale = p_Scale; + v_BorderRadius = i_BorderRadius * u_Scale; + v_BorderWidth = floor(i_BorderWidth * u_Scale); + + gl_Position = u_Transform * i_Transform * vec4(q_Pos, 0.0, 1.0); +} diff --git a/glow/src/shader/triangle.frag b/glow/src/shader/triangle.frag new file mode 100644 index 00000000..d186784a --- /dev/null +++ b/glow/src/shader/triangle.frag @@ -0,0 +1,9 @@ +#version 330 + +in vec4 v_Color; + +out vec4 o_Color; + +void main() { + o_Color = v_Color; +} diff --git a/glow/src/shader/triangle.vert b/glow/src/shader/triangle.vert new file mode 100644 index 00000000..5723436a --- /dev/null +++ b/glow/src/shader/triangle.vert @@ -0,0 +1,13 @@ +#version 330 + +uniform mat4 u_Transform; + +layout(location = 0) in vec2 i_Position; +layout(location = 1) in vec4 i_Color; + +out vec4 v_Color; + +void main() { + gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0); + v_Color = i_Color; +} diff --git a/glow/src/text.rs b/glow/src/text.rs new file mode 100644 index 00000000..6dc7882c --- /dev/null +++ b/glow/src/text.rs @@ -0,0 +1,151 @@ +use crate::Transformation; +use glow_glyph::ab_glyph; +use iced_graphics::font; +use std::{cell::RefCell, collections::HashMap}; + +#[derive(Debug)] +pub struct Pipeline { + draw_brush: RefCell<glow_glyph::GlyphBrush>, + draw_font_map: RefCell<HashMap<String, glow_glyph::FontId>>, + measure_brush: RefCell<glyph_brush::GlyphBrush<()>>, +} + +impl Pipeline { + pub fn new(gl: &glow::Context, default_font: Option<&[u8]>) -> Self { + // TODO: Font customization + let font_source = font::Source::new(); + + let default_font = + default_font.map(|slice| slice.to_vec()).unwrap_or_else(|| { + font_source + .load(&[font::Family::SansSerif, font::Family::Serif]) + .unwrap_or_else(|_| font::FALLBACK.to_vec()) + }); + + let font = ab_glyph::FontArc::try_from_vec(default_font) + .unwrap_or_else(|_| { + log::warn!( + "System font failed to load. Falling back to \ + embedded font..." + ); + + ab_glyph::FontArc::try_from_slice(font::FALLBACK) + .expect("Load fallback font") + }); + + let draw_brush = + glow_glyph::GlyphBrushBuilder::using_font(font.clone()) + .initial_cache_size((2048, 2048)) + .draw_cache_multithread(false) // TODO: Expose as a configuration flag + .build(&gl); + + let measure_brush = + glyph_brush::GlyphBrushBuilder::using_font(font).build(); + + Pipeline { + draw_brush: RefCell::new(draw_brush), + draw_font_map: RefCell::new(HashMap::new()), + measure_brush: RefCell::new(measure_brush), + } + } + + pub fn queue(&mut self, section: glow_glyph::Section<'_>) { + self.draw_brush.borrow_mut().queue(section); + } + + pub fn draw_queued( + &mut self, + gl: &glow::Context, + transformation: Transformation, + region: glow_glyph::Region, + ) { + self.draw_brush + .borrow_mut() + .draw_queued_with_transform_and_scissoring( + gl, + transformation.into(), + region, + ) + .expect("Draw text"); + } + + pub fn measure( + &self, + content: &str, + size: f32, + font: iced_native::Font, + bounds: iced_native::Size, + ) -> (f32, f32) { + use glow_glyph::GlyphCruncher; + + let glow_glyph::FontId(font_id) = self.find_font(font); + + let section = glow_glyph::Section { + bounds: (bounds.width, bounds.height), + text: vec![glow_glyph::Text { + text: content, + scale: size.into(), + font_id: glow_glyph::FontId(font_id), + extra: glow_glyph::Extra::default(), + }], + ..Default::default() + }; + + if let Some(bounds) = + self.measure_brush.borrow_mut().glyph_bounds(section) + { + (bounds.width().ceil(), bounds.height().ceil()) + } else { + (0.0, 0.0) + } + } + + pub fn trim_measurement_cache(&mut self) { + // TODO: We should probably use a `GlyphCalculator` for this. However, + // it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop. + // This makes stuff quite inconvenient. A manual method for trimming the + // cache would make our lives easier. + loop { + let action = self + .measure_brush + .borrow_mut() + .process_queued(|_, _| {}, |_| {}); + + match action { + Ok(_) => break, + Err(glyph_brush::BrushError::TextureTooSmall { suggested }) => { + let (width, height) = suggested; + + self.measure_brush + .borrow_mut() + .resize_texture(width, height); + } + } + } + } + + pub fn find_font(&self, font: iced_native::Font) -> glow_glyph::FontId { + match font { + iced_native::Font::Default => glow_glyph::FontId(0), + iced_native::Font::External { name, bytes } => { + if let Some(font_id) = self.draw_font_map.borrow().get(name) { + return *font_id; + } + + let font = ab_glyph::FontArc::try_from_slice(bytes) + .expect("Load font"); + + let _ = self.measure_brush.borrow_mut().add_font(font.clone()); + + let font_id = self.draw_brush.borrow_mut().add_font(font); + + let _ = self + .draw_font_map + .borrow_mut() + .insert(String::from(name), font_id); + + font_id + } + } + } +} diff --git a/glow/src/triangle.rs b/glow/src/triangle.rs new file mode 100644 index 00000000..ee7faf83 --- /dev/null +++ b/glow/src/triangle.rs @@ -0,0 +1,292 @@ +//! Draw meshes of triangles. +use crate::program; +use crate::Transformation; +use glow::HasContext; +use iced_graphics::layer; +use std::marker::PhantomData; + +pub use iced_graphics::triangle::{Mesh2D, Vertex2D}; + +const VERTEX_BUFFER_SIZE: usize = 10_000; +const INDEX_BUFFER_SIZE: usize = 10_000; + +#[derive(Debug)] +pub(crate) struct Pipeline { + program: <glow::Context as HasContext>::Program, + vertex_array: <glow::Context as HasContext>::VertexArray, + vertices: Buffer<Vertex2D>, + indices: Buffer<u32>, + transform_location: <glow::Context as HasContext>::UniformLocation, + current_transform: Transformation, +} + +impl Pipeline { + pub fn new(gl: &glow::Context) -> Pipeline { + let program = unsafe { + program::create( + gl, + &[ + (glow::VERTEX_SHADER, include_str!("shader/triangle.vert")), + ( + glow::FRAGMENT_SHADER, + include_str!("shader/triangle.frag"), + ), + ], + ) + }; + + let transform_location = + unsafe { gl.get_uniform_location(program, "u_Transform") } + .expect("Get transform location"); + + unsafe { + gl.use_program(Some(program)); + + let transform: [f32; 16] = Transformation::identity().into(); + gl.uniform_matrix_4_f32_slice( + Some(transform_location), + false, + &transform, + ); + + gl.use_program(None); + } + + let vertex_array = + unsafe { gl.create_vertex_array().expect("Create vertex array") }; + + unsafe { + gl.bind_vertex_array(Some(vertex_array)); + } + + let vertices = unsafe { + Buffer::new( + gl, + glow::ARRAY_BUFFER, + glow::DYNAMIC_DRAW, + VERTEX_BUFFER_SIZE, + ) + }; + + let indices = unsafe { + Buffer::new( + gl, + glow::ELEMENT_ARRAY_BUFFER, + glow::DYNAMIC_DRAW, + INDEX_BUFFER_SIZE, + ) + }; + + unsafe { + let stride = std::mem::size_of::<Vertex2D>() as i32; + + gl.enable_vertex_attrib_array(0); + gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, stride, 0); + + gl.enable_vertex_attrib_array(1); + gl.vertex_attrib_pointer_f32( + 1, + 4, + glow::FLOAT, + false, + stride, + 4 * 2, + ); + + gl.bind_vertex_array(None); + } + + Pipeline { + program, + vertex_array, + vertices, + indices, + transform_location, + current_transform: Transformation::identity(), + } + } + + pub fn draw( + &mut self, + gl: &glow::Context, + target_height: u32, + transformation: Transformation, + scale_factor: f32, + meshes: &[layer::Mesh<'_>], + ) { + unsafe { + gl.enable(glow::MULTISAMPLE); + gl.enable(glow::SCISSOR_TEST); + gl.use_program(Some(self.program)); + gl.bind_vertex_array(Some(self.vertex_array)); + } + + // This looks a bit crazy, but we are just counting how many vertices + // and indices we will need to handle. + // TODO: Improve readability + let (total_vertices, total_indices) = meshes + .iter() + .map(|layer::Mesh { buffers, .. }| { + (buffers.vertices.len(), buffers.indices.len()) + }) + .fold((0, 0), |(total_v, total_i), (v, i)| { + (total_v + v, total_i + i) + }); + + // Then we ensure the current buffers are big enough, resizing if + // necessary + unsafe { + self.vertices.bind(gl, total_vertices); + self.indices.bind(gl, total_indices); + } + + // We upload all the vertices and indices upfront + let mut last_vertex = 0; + let mut last_index = 0; + + for layer::Mesh { buffers, .. } in meshes { + unsafe { + gl.buffer_sub_data_u8_slice( + glow::ARRAY_BUFFER, + (last_vertex * std::mem::size_of::<Vertex2D>()) as i32, + bytemuck::cast_slice(&buffers.vertices), + ); + + gl.buffer_sub_data_u8_slice( + glow::ELEMENT_ARRAY_BUFFER, + (last_index * std::mem::size_of::<u32>()) as i32, + bytemuck::cast_slice(&buffers.indices), + ); + + last_vertex += buffers.vertices.len(); + last_index += buffers.indices.len(); + } + } + + // Then we draw each mesh using offsets + let mut last_vertex = 0; + let mut last_index = 0; + + for layer::Mesh { + buffers, + origin, + clip_bounds, + } in meshes + { + let transform = + transformation * Transformation::translate(origin.x, origin.y); + + let clip_bounds = (*clip_bounds * scale_factor).round(); + + unsafe { + if self.current_transform != transform { + let matrix: [f32; 16] = transform.into(); + gl.uniform_matrix_4_f32_slice( + Some(self.transform_location), + false, + &matrix, + ); + + self.current_transform = transform; + } + + gl.scissor( + clip_bounds.x as i32, + (target_height - (clip_bounds.y + clip_bounds.height)) + as i32, + clip_bounds.width as i32, + clip_bounds.height as i32, + ); + + gl.draw_elements_base_vertex( + glow::TRIANGLES, + buffers.indices.len() as i32, + glow::UNSIGNED_INT, + (last_index * std::mem::size_of::<u32>()) as i32, + last_vertex as i32, + ); + + last_vertex += buffers.vertices.len(); + last_index += buffers.indices.len(); + } + } + + unsafe { + gl.bind_vertex_array(None); + gl.use_program(None); + gl.disable(glow::SCISSOR_TEST); + gl.disable(glow::MULTISAMPLE); + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct Uniforms { + transform: [f32; 16], +} + +unsafe impl bytemuck::Zeroable for Uniforms {} +unsafe impl bytemuck::Pod for Uniforms {} + +impl Default for Uniforms { + fn default() -> Self { + Self { + transform: *Transformation::identity().as_ref(), + } + } +} + +impl From<Transformation> for Uniforms { + fn from(transformation: Transformation) -> Uniforms { + Self { + transform: transformation.into(), + } + } +} + +#[derive(Debug)] +struct Buffer<T> { + raw: <glow::Context as HasContext>::Buffer, + target: u32, + usage: u32, + size: usize, + phantom: PhantomData<T>, +} + +impl<T> Buffer<T> { + pub unsafe fn new( + gl: &glow::Context, + target: u32, + usage: u32, + size: usize, + ) -> Self { + let raw = gl.create_buffer().expect("Create buffer"); + + let mut buffer = Buffer { + raw, + target, + usage, + size: 0, + phantom: PhantomData, + }; + + buffer.bind(gl, size); + + buffer + } + + pub unsafe fn bind(&mut self, gl: &glow::Context, size: usize) { + gl.bind_buffer(self.target, Some(self.raw)); + + if self.size < size { + gl.buffer_data_size( + self.target, + (size * std::mem::size_of::<T>()) as i32, + self.usage, + ); + + self.size = size; + } + } +} diff --git a/glow/src/widget.rs b/glow/src/widget.rs new file mode 100644 index 00000000..9968092b --- /dev/null +++ b/glow/src/widget.rs @@ -0,0 +1,58 @@ +//! Use the widgets supported out-of-the-box. +//! +//! # Re-exports +//! For convenience, the contents of this module are available at the root +//! module. Therefore, you can directly type: +//! +//! ``` +//! use iced_glow::{button, Button}; +//! ``` +use crate::Renderer; + +pub mod button; +pub mod checkbox; +pub mod container; +pub mod pane_grid; +pub mod progress_bar; +pub mod radio; +pub mod scrollable; +pub mod slider; +pub mod text_input; + +#[doc(no_inline)] +pub use button::Button; +#[doc(no_inline)] +pub use checkbox::Checkbox; +#[doc(no_inline)] +pub use container::Container; +#[doc(no_inline)] +pub use pane_grid::PaneGrid; +#[doc(no_inline)] +pub use progress_bar::ProgressBar; +#[doc(no_inline)] +pub use radio::Radio; +#[doc(no_inline)] +pub use scrollable::Scrollable; +#[doc(no_inline)] +pub use slider::Slider; +#[doc(no_inline)] +pub use text_input::TextInput; + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub mod canvas; + +#[cfg(feature = "canvas")] +#[doc(no_inline)] +pub use canvas::Canvas; + +pub use iced_native::{Image, Space}; + +/// A container that distributes its contents vertically. +pub type Column<'a, Message> = iced_native::Column<'a, Message, Renderer>; + +/// A container that distributes its contents horizontally. +pub type Row<'a, Message> = iced_native::Row<'a, Message, Renderer>; + +/// A paragraph of text. +pub type Text = iced_native::Text<Renderer>; diff --git a/glow/src/widget/button.rs b/glow/src/widget/button.rs new file mode 100644 index 00000000..fee7a7f8 --- /dev/null +++ b/glow/src/widget/button.rs @@ -0,0 +1,15 @@ +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Button`] has some local [`State`]. +//! +//! [`Button`]: type.Button.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_graphics::button::{Style, StyleSheet}; +pub use iced_native::button::State; + +/// A widget that produces a message when clicked. +/// +/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. +pub type Button<'a, Message> = iced_native::Button<'a, Message, Renderer>; diff --git a/glow/src/widget/canvas.rs b/glow/src/widget/canvas.rs new file mode 100644 index 00000000..bef34857 --- /dev/null +++ b/glow/src/widget/canvas.rs @@ -0,0 +1,9 @@ +//! Draw 2D graphics for your users. +//! +//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a +//! [`Frame`]. It can be used for animation, data visualization, game graphics, +//! and more! +//! +//! [`Canvas`]: struct.Canvas.html +//! [`Frame`]: struct.Frame.html +pub use iced_graphics::canvas::*; diff --git a/glow/src/widget/checkbox.rs b/glow/src/widget/checkbox.rs new file mode 100644 index 00000000..d27d77cc --- /dev/null +++ b/glow/src/widget/checkbox.rs @@ -0,0 +1,9 @@ +//! Show toggle controls using checkboxes. +use crate::Renderer; + +pub use iced_graphics::checkbox::{Style, StyleSheet}; + +/// A box that can be checked. +/// +/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. +pub type Checkbox<Message> = iced_native::Checkbox<Message, Renderer>; diff --git a/glow/src/widget/container.rs b/glow/src/widget/container.rs new file mode 100644 index 00000000..bc26cef2 --- /dev/null +++ b/glow/src/widget/container.rs @@ -0,0 +1,10 @@ +//! Decorate content and apply alignment. +use crate::Renderer; + +pub use iced_graphics::container::{Style, StyleSheet}; + +/// An element decorating some content. +/// +/// This is an alias of an `iced_native` container with a default +/// `Renderer`. +pub type Container<'a, Message> = iced_native::Container<'a, Message, Renderer>; diff --git a/glow/src/widget/pane_grid.rs b/glow/src/widget/pane_grid.rs new file mode 100644 index 00000000..578e8960 --- /dev/null +++ b/glow/src/widget/pane_grid.rs @@ -0,0 +1,24 @@ +//! Let your users split regions of your application and organize layout dynamically. +//! +//! [](https://gfycat.com/mixedflatjellyfish) +//! +//! # Example +//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, +//! drag and drop, and hotkey support. +//! +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.1/examples/pane_grid +//! [`PaneGrid`]: type.PaneGrid.html +use crate::Renderer; + +pub use iced_native::pane_grid::{ + Axis, Direction, DragEvent, Focus, KeyPressEvent, Pane, ResizeEvent, Split, + State, +}; + +/// A collection of panes distributed using either vertical or horizontal splits +/// to completely fill the space available. +/// +/// [](https://gfycat.com/mixedflatjellyfish) +/// +/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. +pub type PaneGrid<'a, Message> = iced_native::PaneGrid<'a, Message, Renderer>; diff --git a/glow/src/widget/progress_bar.rs b/glow/src/widget/progress_bar.rs new file mode 100644 index 00000000..5782103c --- /dev/null +++ b/glow/src/widget/progress_bar.rs @@ -0,0 +1,15 @@ +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Button`] has some local [`State`]. +//! +//! [`Button`]: type.Button.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_graphics::progress_bar::{Style, StyleSheet}; + +/// A bar that displays progress. +/// +/// This is an alias of an `iced_native` progress bar with an +/// `iced_wgpu::Renderer`. +pub type ProgressBar = iced_native::ProgressBar<Renderer>; diff --git a/glow/src/widget/radio.rs b/glow/src/widget/radio.rs new file mode 100644 index 00000000..0b843d1f --- /dev/null +++ b/glow/src/widget/radio.rs @@ -0,0 +1,10 @@ +//! Create choices using radio buttons. +use crate::Renderer; + +pub use iced_graphics::radio::{Style, StyleSheet}; + +/// A circular button representing a choice. +/// +/// This is an alias of an `iced_native` radio button with an +/// `iced_wgpu::Renderer`. +pub type Radio<Message> = iced_native::Radio<Message, Renderer>; diff --git a/glow/src/widget/scrollable.rs b/glow/src/widget/scrollable.rs new file mode 100644 index 00000000..fabb4318 --- /dev/null +++ b/glow/src/widget/scrollable.rs @@ -0,0 +1,13 @@ +//! Navigate an endless amount of content with a scrollbar. +use crate::Renderer; + +pub use iced_graphics::scrollable::{Scrollbar, Scroller, StyleSheet}; +pub use iced_native::scrollable::State; + +/// A widget that can vertically display an infinite amount of content +/// with a scrollbar. +/// +/// This is an alias of an `iced_native` scrollable with a default +/// `Renderer`. +pub type Scrollable<'a, Message> = + iced_native::Scrollable<'a, Message, Renderer>; diff --git a/glow/src/widget/slider.rs b/glow/src/widget/slider.rs new file mode 100644 index 00000000..cf036829 --- /dev/null +++ b/glow/src/widget/slider.rs @@ -0,0 +1,16 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`Slider`] has some local [`State`]. +//! +//! [`Slider`]: struct.Slider.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_graphics::slider::{Handle, HandleShape, Style, StyleSheet}; +pub use iced_native::slider::State; + +/// An horizontal bar and a handle that selects a single value from a range of +/// values. +/// +/// This is an alias of an `iced_native` slider with an `iced_wgpu::Renderer`. +pub type Slider<'a, Message> = iced_native::Slider<'a, Message, Renderer>; diff --git a/glow/src/widget/text_input.rs b/glow/src/widget/text_input.rs new file mode 100644 index 00000000..1da3fbe6 --- /dev/null +++ b/glow/src/widget/text_input.rs @@ -0,0 +1,15 @@ +//! Display fields that can be filled with text. +//! +//! A [`TextInput`] has some local [`State`]. +//! +//! [`TextInput`]: struct.TextInput.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_graphics::text_input::{Style, StyleSheet}; +pub use iced_native::text_input::State; + +/// A field that can be filled with text. +/// +/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. +pub type TextInput<'a, Message> = iced_native::TextInput<'a, Message, Renderer>; diff --git a/glow/src/window.rs b/glow/src/window.rs new file mode 100644 index 00000000..aac5fb9e --- /dev/null +++ b/glow/src/window.rs @@ -0,0 +1,4 @@ +//! Display rendering results on windows. +mod compositor; + +pub use compositor::Compositor; diff --git a/glow/src/window/compositor.rs b/glow/src/window/compositor.rs new file mode 100644 index 00000000..2f504ff7 --- /dev/null +++ b/glow/src/window/compositor.rs @@ -0,0 +1,74 @@ +use crate::{Backend, Renderer, Settings, Viewport}; + +use core::ffi::c_void; +use glow::HasContext; +use iced_graphics::{Antialiasing, Size}; +use iced_native::mouse; + +/// A window graphics backend for iced powered by `glow`. +#[allow(missing_debug_implementations)] +pub struct Compositor { + gl: glow::Context, +} + +impl iced_graphics::window::GLCompositor for Compositor { + type Settings = Settings; + type Renderer = Renderer; + + unsafe fn new( + settings: Self::Settings, + loader_function: impl FnMut(&str) -> *const c_void, + ) -> (Self, Self::Renderer) { + let gl = glow::Context::from_loader_function(loader_function); + + gl.clear_color(1.0, 1.0, 1.0, 1.0); + + // Enable auto-conversion from/to sRGB + gl.enable(glow::FRAMEBUFFER_SRGB); + + // Enable alpha blending + gl.enable(glow::BLEND); + gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA); + + // Disable multisampling by default + gl.disable(glow::MULTISAMPLE); + + let renderer = Renderer::new(Backend::new(&gl, settings)); + + (Self { gl }, renderer) + } + + fn sample_count(settings: &Settings) -> u32 { + settings + .antialiasing + .map(Antialiasing::sample_count) + .unwrap_or(0) + } + + fn resize_viewport(&mut self, physical_size: Size<u32>) { + unsafe { + self.gl.viewport( + 0, + 0, + physical_size.width as i32, + physical_size.height as i32, + ); + } + } + + fn draw<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + viewport: &Viewport, + output: &<Self::Renderer as iced_native::Renderer>::Output, + overlay: &[T], + ) -> mouse::Interaction { + let gl = &self.gl; + + unsafe { + gl.clear(glow::COLOR_BUFFER_BIT); + } + + renderer.backend_mut().draw(gl, viewport, output, overlay) + } +} |