diff options
51 files changed, 3213 insertions, 43 deletions
@@ -34,6 +34,7 @@ maintenance = { status = "actively-developed" } members = [ "core", "futures", + "glow", "native", "style", "web", diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml index 96749e90..b34c5feb 100644 --- a/examples/tour/Cargo.toml +++ b/examples/tour/Cargo.toml @@ -6,5 +6,6 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../..", features = ["image", "debug"] } +iced_winit = { path = "../../winit", features = ["debug"] } +iced_glow = { path = "../../glow" } env_logger = "0.7" diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index c9678b9d..729ae8fb 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,13 +1,20 @@ -use iced::{ - button, scrollable, slider, text_input, Button, Checkbox, Color, Column, - Container, Element, HorizontalAlignment, Image, Length, Radio, Row, - Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput, +use iced_glow::{ + button, scrollable, slider, text_input, window, Button, Checkbox, Color, + Column, Command, Container, Element, HorizontalAlignment, Image, Length, + Radio, Row, Scrollable, Slider, Space, Text, TextInput, }; +use iced_winit::{executor, Application, Settings}; pub fn main() { env_logger::init(); - Tour::run(Settings::default()) + Tour::run( + Settings::default(), + iced_glow::Settings { + default_font: None, + antialiasing: None, + }, + ) } pub struct Tour { @@ -18,24 +25,30 @@ pub struct Tour { debug: bool, } -impl Sandbox for Tour { +impl Application for Tour { + type Backend = window::Backend; + type Executor = executor::Null; type Message = Message; - - fn new() -> Tour { - Tour { - steps: Steps::new(), - scroll: scrollable::State::new(), - back_button: button::State::new(), - next_button: button::State::new(), - debug: false, - } + type Flags = (); + + fn new(_flags: ()) -> (Tour, Command<Message>) { + ( + Tour { + steps: Steps::new(), + scroll: scrollable::State::new(), + back_button: button::State::new(), + next_button: button::State::new(), + debug: false, + }, + Command::none(), + ) } fn title(&self) -> String { format!("{} - Iced", self.steps.title()) } - fn update(&mut self, event: Message) { + fn update(&mut self, event: Message) -> Command<Message> { match event { Message::BackPressed => { self.steps.go_back(); @@ -47,6 +60,8 @@ impl Sandbox for Tour { self.steps.update(step_msg, &mut self.debug); } } + + Command::none() } fn view(&mut self) -> Element<Message> { @@ -678,17 +693,18 @@ impl<'a> Step { fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { Container::new( + Text::new("Not supported yet!") // This should go away once we unify resource loading on native // platforms - if cfg!(target_arch = "wasm32") { - Image::new("images/ferris.png") - } else { - Image::new(format!( - "{}/images/ferris.png", - env!("CARGO_MANIFEST_DIR") - )) - } - .width(Length::Units(width)), + //if cfg!(target_arch = "wasm32") { + // Image::new("images/ferris.png") + //} else { + // Image::new(format!( + // "{}/images/ferris.png", + // env!("CARGO_MANIFEST_DIR") + // )) + //} + //.width(Length::Units(width)), ) .width(Length::Fill) .center_x() @@ -749,7 +765,7 @@ pub enum Layout { } mod style { - use iced::{button, Background, Color, Vector}; + use iced_glow::{button, Background, Color, Vector}; pub enum Button { Primary, diff --git a/glow/Cargo.toml b/glow/Cargo.toml new file mode 100644 index 00000000..e130d563 --- /dev/null +++ b/glow/Cargo.toml @@ -0,0 +1,34 @@ +[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" + +[dependencies] +raw-window-handle = "0.3" +euclid = "0.20" +glow = "0.4" +bytemuck = "1.2" +glam = "0.8" +font-kit = "0.6" +log = "0.4" +glyph_brush = "0.6" + +[dependencies.iced_native] +version = "0.2" +path = "../native" + +[dependencies.iced_style] +version = "0.1" +path = "../style" + +[dependencies.surfman] +path = "../../surfman/surfman" +default-features = false +features = ["sm-raw-window-handle", "sm-x11"] + +[dependencies.glow_glyph] +path = "../../glow_glyph" diff --git a/glow/src/defaults.rs b/glow/src/defaults.rs new file mode 100644 index 00000000..11718a87 --- /dev/null +++ b/glow/src/defaults.rs @@ -0,0 +1,32 @@ +//! Use default styling attributes to inherit styles. +use iced_native::Color; + +/// Some default styling attributes. +#[derive(Debug, Clone, Copy)] +pub struct Defaults { + /// Text styling + pub text: Text, +} + +impl Default for Defaults { + fn default() -> Defaults { + Defaults { + text: Text::default(), + } + } +} + +/// Some default text styling attributes. +#[derive(Debug, Clone, Copy)] +pub struct Text { + /// The default color of text + pub color: Color, +} + +impl Default for Text { + fn default() -> Text { + Text { + color: Color::BLACK, + } + } +} diff --git a/glow/src/lib.rs b/glow/src/lib.rs new file mode 100644 index 00000000..ce447192 --- /dev/null +++ b/glow/src/lib.rs @@ -0,0 +1,37 @@ +//#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(unused_results)] +//#![forbid(unsafe_code)] +#![forbid(rust_2018_idioms)] + +mod defaults; +mod primitive; +mod quad; +mod renderer; +mod text; +mod transformation; +mod triangle; +mod viewport; + +pub mod settings; +pub mod widget; +pub mod window; + +pub use defaults::Defaults; +pub use primitive::Primitive; +pub use renderer::Renderer; +pub use settings::Settings; +pub use viewport::Viewport; + +pub(crate) use quad::Quad; +pub(crate) use transformation::Transformation; + +#[doc(no_inline)] +pub use widget::*; + +pub use iced_native::{ + Background, Color, Command, HorizontalAlignment, Length, Vector, + VerticalAlignment, +}; + +pub type Element<'a, Message> = iced_native::Element<'a, Message, Renderer>; diff --git a/glow/src/primitive.rs b/glow/src/primitive.rs new file mode 100644 index 00000000..e73227ef --- /dev/null +++ b/glow/src/primitive.rs @@ -0,0 +1,107 @@ +use iced_native::{ + image, svg, Background, Color, Font, HorizontalAlignment, Rectangle, Size, + Vector, VerticalAlignment, +}; + +use crate::triangle; +use std::sync::Arc; + +/// A rendering primitive. +#[derive(Debug, Clone)] +pub enum Primitive { + /// An empty primitive + None, + /// A group of primitives + Group { + /// The primitives of the group + primitives: Vec<Primitive>, + }, + /// A text primitive + Text { + /// The contents of the text + content: String, + /// The bounds of the text + bounds: Rectangle, + /// The color of the text + color: Color, + /// The size of the text + size: f32, + /// The font of the text + font: Font, + /// The horizontal alignment of the text + horizontal_alignment: HorizontalAlignment, + /// The vertical alignment of the text + vertical_alignment: VerticalAlignment, + }, + /// A quad primitive + Quad { + /// The bounds of the quad + bounds: Rectangle, + /// The background of the quad + background: Background, + /// The border radius of the quad + border_radius: u16, + /// The border width of the quad + border_width: u16, + /// The border color of the quad + border_color: Color, + }, + /// An image primitive + Image { + /// The handle of the image + handle: image::Handle, + /// The bounds of the image + bounds: Rectangle, + }, + /// An SVG primitive + Svg { + /// The path of the SVG file + handle: svg::Handle, + + /// The bounds of the viewport + bounds: Rectangle, + }, + /// A clip primitive + Clip { + /// The bounds of the clip + bounds: Rectangle, + /// The offset transformation of the clip + offset: Vector<u32>, + /// The content of the clip + content: Box<Primitive>, + }, + /// A primitive that applies a translation + Translate { + /// The translation vector + translation: Vector, + + /// The primitive to translate + content: Box<Primitive>, + }, + /// A low-level primitive to render a mesh of triangles. + /// + /// It can be used to render many kinds of geometry freely. + Mesh2D { + /// The size of the drawable region of the mesh. + /// + /// Any geometry that falls out of this region will be clipped. + size: Size, + + /// The vertex and index buffers of the mesh + buffers: triangle::Mesh2D, + }, + /// A cached primitive. + /// + /// This can be useful if you are implementing a widget where primitive + /// generation is expensive. + Cached { + /// The cached primitive + cache: Arc<Primitive>, + }, +} + +impl Default for Primitive { + fn default() -> Primitive { + Primitive::None + } +} diff --git a/glow/src/quad.rs b/glow/src/quad.rs new file mode 100644 index 00000000..744597d2 --- /dev/null +++ b/glow/src/quad.rs @@ -0,0 +1,254 @@ +use crate::{Transformation, Viewport}; +use glow::HasContext; +use iced_native::Rectangle; + +#[derive(Debug)] +pub struct Pipeline { + program: <glow::Context as HasContext>::Program, + vertex_array: <glow::Context as HasContext>::VertexArray, + instances: <glow::Context as HasContext>::Buffer, + current_transform: Transformation, + current_scale: f32, +} + +impl Pipeline { + pub fn new(gl: &glow::Context) -> Pipeline { + let program = unsafe { + create_program( + gl, + &[ + (glow::VERTEX_SHADER, include_str!("shader/quad.vert")), + (glow::FRAGMENT_SHADER, include_str!("shader/quad.frag")), + ], + ) + }; + + unsafe { + gl.use_program(Some(program)); + + gl.uniform_matrix_4_f32_slice( + Some(0), + false, + &Transformation::identity().into(), + ); + gl.uniform_1_f32(Some(1), 1.0); + + gl.use_program(None); + } + + let (vertex_array, instances) = + unsafe { create_instance_buffer(gl, Quad::MAX) }; + + Pipeline { + program, + vertex_array, + instances, + current_transform: Transformation::identity(), + current_scale: 1.0, + } + } + + pub fn draw( + &mut self, + gl: &glow::Context, + viewport: &Viewport, + instances: &[Quad], + transformation: Transformation, + scale: f32, + bounds: Rectangle<u32>, + ) { + unsafe { + gl.enable(glow::SCISSOR_TEST); + gl.scissor( + bounds.x as i32, + (viewport.height() + - (bounds.y + bounds.height).min(viewport.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 { + gl.uniform_matrix_4_f32_slice( + Some(0), + false, + &transformation.into(), + ); + + self.current_transform = transformation; + } + } + + if scale != self.current_scale { + unsafe { + gl.uniform_1_f32(Some(1), scale); + } + + self.current_scale = scale; + } + + let mut i = 0; + let total = instances.len(); + + while i < total { + let end = (i + Quad::MAX).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 += Quad::MAX; + } + + unsafe { + gl.bind_vertex_array(None); + gl.use_program(None); + gl.disable(glow::SCISSOR_TEST); + } + } +} + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct Quad { + pub position: [f32; 2], + pub scale: [f32; 2], + pub color: [f32; 4], + pub border_color: [f32; 4], + pub border_radius: f32, + pub border_width: f32, +} + +unsafe impl bytemuck::Zeroable for Quad {} +unsafe impl bytemuck::Pod for Quad {} + +impl Quad { + const MAX: usize = 100_000; +} + +unsafe fn create_program( + 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 +} + +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::<Quad>()) as i32, + glow::DYNAMIC_DRAW, + ); + + let stride = std::mem::size_of::<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/renderer.rs b/glow/src/renderer.rs new file mode 100644 index 00000000..40228a6b --- /dev/null +++ b/glow/src/renderer.rs @@ -0,0 +1,455 @@ +use crate::{ + quad, text, triangle, Defaults, Primitive, Quad, Settings, Transformation, + Viewport, +}; + +use iced_native::{ + layout, mouse, Background, Color, Layout, Point, Rectangle, Vector, Widget, +}; + +mod widget; + +/// A [`glow`] renderer. +/// +/// [`glow`]: https://github.com/grovesNL/glow +#[derive(Debug)] +pub struct Renderer { + quad_pipeline: quad::Pipeline, + text_pipeline: text::Pipeline, + triangle_pipeline: triangle::Pipeline, +} + +struct Layer<'a> { + bounds: Rectangle<u32>, + quads: Vec<Quad>, + text: Vec<glow_glyph::Section<'a>>, + meshes: Vec<(Vector, Rectangle<u32>, &'a triangle::Mesh2D)>, +} + +impl<'a> Layer<'a> { + pub fn new(bounds: Rectangle<u32>) -> Self { + Self { + bounds, + quads: Vec::new(), + text: Vec::new(), + meshes: Vec::new(), + } + } + + pub fn intersection(&self, rectangle: Rectangle) -> Option<Rectangle<u32>> { + let layer_bounds: Rectangle<f32> = self.bounds.into(); + + layer_bounds.intersection(&rectangle).map(Into::into) + } +} + +impl Renderer { + /// 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, settings.antialiasing); + + Self { + quad_pipeline, + text_pipeline, + triangle_pipeline, + } + } + + /// Draws the provided primitives in the given [`Target`]. + /// + /// The text provided as overlay will be renderer on top of the primitives. + /// This is useful for rendering debug information. + /// + /// [`Target`]: struct.Target.html + pub fn draw<T: AsRef<str>>( + &mut self, + gl: &glow::Context, + viewport: &Viewport, + (primitive, mouse_interaction): &(Primitive, mouse::Interaction), + scale_factor: f64, + overlay: &[T], + ) -> mouse::Interaction { + let (width, height) = viewport.dimensions(); + let scale_factor = scale_factor as f32; + let transformation = viewport.transformation(); + + let mut layers = Vec::new(); + + layers.push(Layer::new(Rectangle { + x: 0, + y: 0, + width: (width as f32 / scale_factor).round() as u32, + height: (height as f32 / scale_factor).round() as u32, + })); + + self.draw_primitive(Vector::new(0.0, 0.0), primitive, &mut layers); + self.draw_overlay(overlay, &mut layers); + + for layer in layers { + self.flush( + gl, + viewport, + scale_factor, + transformation, + &layer, + width, + height, + ); + } + + *mouse_interaction + } + + fn draw_primitive<'a>( + &mut self, + translation: Vector, + primitive: &'a Primitive, + layers: &mut Vec<Layer<'a>>, + ) { + match primitive { + Primitive::None => {} + Primitive::Group { primitives } => { + // TODO: Inspect a bit and regroup (?) + for primitive in primitives { + self.draw_primitive(translation, primitive, layers) + } + } + Primitive::Text { + content, + bounds, + size, + color, + font, + horizontal_alignment, + vertical_alignment, + } => { + let layer = layers.last_mut().unwrap(); + + layer.text.push(glow_glyph::Section { + text: &content, + screen_position: ( + bounds.x + translation.x, + bounds.y + translation.y, + ), + bounds: (bounds.width, bounds.height), + scale: glow_glyph::Scale { x: *size, y: *size }, + color: color.into_linear(), + font_id: self.text_pipeline.find_font(*font), + layout: glow_glyph::Layout::default() + .h_align(match horizontal_alignment { + iced_native::HorizontalAlignment::Left => { + glow_glyph::HorizontalAlign::Left + } + iced_native::HorizontalAlignment::Center => { + glow_glyph::HorizontalAlign::Center + } + iced_native::HorizontalAlignment::Right => { + glow_glyph::HorizontalAlign::Right + } + }) + .v_align(match vertical_alignment { + iced_native::VerticalAlignment::Top => { + glow_glyph::VerticalAlign::Top + } + iced_native::VerticalAlignment::Center => { + glow_glyph::VerticalAlign::Center + } + iced_native::VerticalAlignment::Bottom => { + glow_glyph::VerticalAlign::Bottom + } + }), + ..Default::default() + }) + } + Primitive::Quad { + bounds, + background, + border_radius, + border_width, + border_color, + } => { + let layer = layers.last_mut().unwrap(); + + // TODO: Move some of these computations to the GPU (?) + layer.quads.push(Quad { + position: [ + bounds.x + translation.x, + bounds.y + translation.y, + ], + scale: [bounds.width, bounds.height], + color: match background { + Background::Color(color) => color.into_linear(), + }, + border_radius: *border_radius as f32, + border_width: *border_width as f32, + border_color: border_color.into_linear(), + }); + } + Primitive::Mesh2D { size, buffers } => { + let layer = layers.last_mut().unwrap(); + + // Only draw visible content + if let Some(clip_bounds) = layer.intersection(Rectangle::new( + Point::new(translation.x, translation.y), + *size, + )) { + layer.meshes.push(( + translation, + clip_bounds.into(), + buffers, + )); + } + } + Primitive::Clip { + bounds, + offset, + content, + } => { + let layer = layers.last_mut().unwrap(); + + // Only draw visible content + if let Some(clip_bounds) = + layer.intersection(*bounds + translation) + { + let clip_layer = Layer::new(clip_bounds.into()); + let new_layer = Layer::new(layer.bounds); + + layers.push(clip_layer); + self.draw_primitive( + translation + - Vector::new(offset.x as f32, offset.y as f32), + content, + layers, + ); + layers.push(new_layer); + } + } + Primitive::Translate { + translation: new_translation, + content, + } => { + self.draw_primitive( + translation + *new_translation, + &content, + layers, + ); + } + + Primitive::Cached { cache } => { + self.draw_primitive(translation, &cache, layers); + } + + #[cfg(feature = "image")] + Primitive::Image { handle, bounds } => { + let layer = layers.last_mut().unwrap(); + + layer.images.push(Image { + handle: image::Handle::Raster(handle.clone()), + position: [ + bounds.x + translation.x, + bounds.y + translation.y, + ], + size: [bounds.width, bounds.height], + }); + } + #[cfg(not(feature = "image"))] + Primitive::Image { .. } => {} + + #[cfg(feature = "svg")] + Primitive::Svg { handle, bounds } => { + let layer = layers.last_mut().unwrap(); + + layer.images.push(Image { + handle: image::Handle::Vector(handle.clone()), + position: [ + bounds.x + translation.x, + bounds.y + translation.y, + ], + size: [bounds.width, bounds.height], + }); + } + #[cfg(not(feature = "svg"))] + Primitive::Svg { .. } => {} + } + } + + fn draw_overlay<'a, T: AsRef<str>>( + &mut self, + lines: &'a [T], + layers: &mut Vec<Layer<'a>>, + ) { + let first = layers.first().unwrap(); + let mut overlay = Layer::new(first.bounds); + + let font_id = self.text_pipeline.overlay_font(); + let scale = glow_glyph::Scale { x: 20.0, y: 20.0 }; + + for (i, line) in lines.iter().enumerate() { + overlay.text.push(glow_glyph::Section { + text: line.as_ref(), + screen_position: (11.0, 11.0 + 25.0 * i as f32), + color: [0.9, 0.9, 0.9, 1.0], + scale, + font_id, + ..glow_glyph::Section::default() + }); + + overlay.text.push(glow_glyph::Section { + text: line.as_ref(), + screen_position: (10.0, 10.0 + 25.0 * i as f32), + color: [0.0, 0.0, 0.0, 1.0], + scale, + font_id, + ..glow_glyph::Section::default() + }); + } + + layers.push(overlay); + } + + fn flush( + &mut self, + gl: &glow::Context, + viewport: &Viewport, + scale_factor: f32, + transformation: Transformation, + layer: &Layer<'_>, + target_width: u32, + target_height: u32, + ) { + let bounds = layer.bounds * scale_factor; + + if !layer.quads.is_empty() { + self.quad_pipeline.draw( + gl, + viewport, + &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_width, + 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.screen_position.0 * scale_factor).round(), + (text.screen_position.1 * 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.0 * scale_factor).ceil(), + (text.bounds.1 * scale_factor).ceil(), + ), + scale: glow_glyph::Scale { + x: text.scale.x * scale_factor, + y: text.scale.y * scale_factor, + }, + ..*text + }; + + self.text_pipeline.queue(text); + } + + self.text_pipeline.draw_queued( + gl, + transformation, + glow_glyph::Region { + x: bounds.x, + y: viewport.height() + - (bounds.y + bounds.height).min(viewport.height()), + width: bounds.width, + height: bounds.height, + }, + ); + } + } +} + +impl iced_native::Renderer for Renderer { + type Output = (Primitive, mouse::Interaction); + type Defaults = Defaults; + + fn layout<'a, Message>( + &mut self, + element: &iced_native::Element<'a, Message, Self>, + limits: &iced_native::layout::Limits, + ) -> iced_native::layout::Node { + let node = element.layout(self, limits); + + self.text_pipeline.clear_measurement_cache(); + + node + } +} + +impl layout::Debugger for Renderer { + fn explain<Message>( + &mut self, + defaults: &Defaults, + widget: &dyn Widget<Message, Self>, + layout: Layout<'_>, + cursor_position: Point, + color: Color, + ) -> Self::Output { + let mut primitives = Vec::new(); + let (primitive, cursor) = + widget.draw(self, defaults, layout, cursor_position); + + explain_layout(layout, color, &mut primitives); + primitives.push(primitive); + + (Primitive::Group { primitives }, cursor) + } +} + +fn explain_layout( + layout: Layout<'_>, + color: Color, + primitives: &mut Vec<Primitive>, +) { + primitives.push(Primitive::Quad { + bounds: layout.bounds(), + background: Background::Color(Color::TRANSPARENT), + border_radius: 0, + border_width: 1, + border_color: [0.6, 0.6, 0.6, 0.5].into(), + }); + + for child in layout.children() { + explain_layout(child, color, primitives); + } +} diff --git a/glow/src/renderer/widget.rs b/glow/src/renderer/widget.rs new file mode 100644 index 00000000..37421fbe --- /dev/null +++ b/glow/src/renderer/widget.rs @@ -0,0 +1,19 @@ +mod button; +mod checkbox; +mod column; +mod container; +mod pane_grid; +mod progress_bar; +mod radio; +mod row; +mod scrollable; +mod slider; +mod space; +mod text; +mod text_input; + +#[cfg(feature = "svg")] +mod svg; + +#[cfg(feature = "image")] +mod image; diff --git a/glow/src/renderer/widget/button.rs b/glow/src/renderer/widget/button.rs new file mode 100644 index 00000000..eb225038 --- /dev/null +++ b/glow/src/renderer/widget/button.rs @@ -0,0 +1,93 @@ +use crate::{button::StyleSheet, defaults, Defaults, Primitive, Renderer}; +use iced_native::{ + mouse, Background, Color, Element, Layout, Point, Rectangle, Vector, +}; + +impl iced_native::button::Renderer for Renderer { + const DEFAULT_PADDING: u16 = 5; + + type Style = Box<dyn StyleSheet>; + + fn draw<Message>( + &mut self, + _defaults: &Defaults, + bounds: Rectangle, + cursor_position: Point, + is_disabled: bool, + is_pressed: bool, + style: &Box<dyn StyleSheet>, + content: &Element<'_, Message, Self>, + content_layout: Layout<'_>, + ) -> Self::Output { + let is_mouse_over = bounds.contains(cursor_position); + + let styling = if is_disabled { + style.disabled() + } else if is_mouse_over { + if is_pressed { + style.pressed() + } else { + style.hovered() + } + } else { + style.active() + }; + + let (content, _) = content.draw( + self, + &Defaults { + text: defaults::Text { + color: styling.text_color, + }, + }, + content_layout, + cursor_position, + ); + + ( + if styling.background.is_some() || styling.border_width > 0 { + let background = Primitive::Quad { + bounds, + background: styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: styling.border_radius, + border_width: styling.border_width, + border_color: styling.border_color, + }; + + if styling.shadow_offset == Vector::default() { + Primitive::Group { + primitives: vec![background, content], + } + } else { + // TODO: Implement proper shadow support + let shadow = Primitive::Quad { + bounds: Rectangle { + x: bounds.x + styling.shadow_offset.x, + y: bounds.y + styling.shadow_offset.y, + ..bounds + }, + background: Background::Color( + [0.0, 0.0, 0.0, 0.5].into(), + ), + border_radius: styling.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }; + + Primitive::Group { + primitives: vec![shadow, background, content], + } + } + } else { + content + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/glow/src/renderer/widget/checkbox.rs b/glow/src/renderer/widget/checkbox.rs new file mode 100644 index 00000000..0340bf62 --- /dev/null +++ b/glow/src/renderer/widget/checkbox.rs @@ -0,0 +1,63 @@ +use crate::{checkbox::StyleSheet, Primitive, Renderer}; +use iced_native::{ + checkbox, mouse, HorizontalAlignment, Rectangle, VerticalAlignment, +}; + +impl checkbox::Renderer for Renderer { + type Style = Box<dyn StyleSheet>; + + const DEFAULT_SIZE: u16 = 20; + const DEFAULT_SPACING: u16 = 15; + + fn draw( + &mut self, + bounds: Rectangle, + is_checked: bool, + is_mouse_over: bool, + (label, _): Self::Output, + style_sheet: &Self::Style, + ) -> Self::Output { + let style = if is_mouse_over { + style_sheet.hovered(is_checked) + } else { + style_sheet.active(is_checked) + }; + + let checkbox = Primitive::Quad { + bounds, + background: style.background, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }; + + ( + Primitive::Group { + primitives: if is_checked { + let check = Primitive::Text { + content: crate::text::CHECKMARK_ICON.to_string(), + font: crate::text::BUILTIN_ICONS, + size: bounds.height * 0.7, + bounds: Rectangle { + x: bounds.center_x(), + y: bounds.center_y(), + ..bounds + }, + color: style.checkmark_color, + horizontal_alignment: HorizontalAlignment::Center, + vertical_alignment: VerticalAlignment::Center, + }; + + vec![checkbox, check, label] + } else { + vec![checkbox, label] + }, + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/glow/src/renderer/widget/column.rs b/glow/src/renderer/widget/column.rs new file mode 100644 index 00000000..b853276d --- /dev/null +++ b/glow/src/renderer/widget/column.rs @@ -0,0 +1,34 @@ +use crate::{Primitive, Renderer}; +use iced_native::{column, mouse, Element, Layout, Point}; + +impl column::Renderer for Renderer { + fn draw<Message>( + &mut self, + defaults: &Self::Defaults, + content: &[Element<'_, Message, Self>], + layout: Layout<'_>, + cursor_position: Point, + ) -> Self::Output { + let mut mouse_interaction = mouse::Interaction::default(); + + ( + Primitive::Group { + primitives: content + .iter() + .zip(layout.children()) + .map(|(child, layout)| { + let (primitive, new_mouse_interaction) = + child.draw(self, defaults, layout, cursor_position); + + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + + primitive + }) + .collect(), + }, + mouse_interaction, + ) + } +} diff --git a/glow/src/renderer/widget/container.rs b/glow/src/renderer/widget/container.rs new file mode 100644 index 00000000..30cc3f07 --- /dev/null +++ b/glow/src/renderer/widget/container.rs @@ -0,0 +1,48 @@ +use crate::{container, defaults, Defaults, Primitive, Renderer}; +use iced_native::{Background, Color, Element, Layout, Point, Rectangle}; + +impl iced_native::container::Renderer for Renderer { + type Style = Box<dyn container::StyleSheet>; + + fn draw<Message>( + &mut self, + defaults: &Defaults, + bounds: Rectangle, + cursor_position: Point, + style_sheet: &Self::Style, + content: &Element<'_, Message, Self>, + content_layout: Layout<'_>, + ) -> Self::Output { + let style = style_sheet.style(); + + let defaults = Defaults { + text: defaults::Text { + color: style.text_color.unwrap_or(defaults.text.color), + }, + }; + + let (content, mouse_interaction) = + content.draw(self, &defaults, content_layout, cursor_position); + + if style.background.is_some() || style.border_width > 0 { + let quad = Primitive::Quad { + bounds, + background: style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }; + + ( + Primitive::Group { + primitives: vec![quad, content], + }, + mouse_interaction, + ) + } else { + (content, mouse_interaction) + } + } +} diff --git a/glow/src/renderer/widget/image.rs b/glow/src/renderer/widget/image.rs new file mode 100644 index 00000000..c4c04984 --- /dev/null +++ b/glow/src/renderer/widget/image.rs @@ -0,0 +1,22 @@ +use crate::{Primitive, Renderer}; +use iced_native::{image, mouse, Layout}; + +impl image::Renderer for Renderer { + fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { + self.image_pipeline.dimensions(handle) + } + + fn draw( + &mut self, + handle: image::Handle, + layout: Layout<'_>, + ) -> Self::Output { + ( + Primitive::Image { + handle, + bounds: layout.bounds(), + }, + mouse::Interaction::default(), + ) + } +} diff --git a/glow/src/renderer/widget/pane_grid.rs b/glow/src/renderer/widget/pane_grid.rs new file mode 100644 index 00000000..2253e4af --- /dev/null +++ b/glow/src/renderer/widget/pane_grid.rs @@ -0,0 +1,93 @@ +use crate::{Primitive, Renderer}; +use iced_native::{ + mouse, + pane_grid::{self, Axis, Pane}, + Element, Layout, Point, Rectangle, Vector, +}; + +impl pane_grid::Renderer for Renderer { + fn draw<Message>( + &mut self, + defaults: &Self::Defaults, + content: &[(Pane, Element<'_, Message, Self>)], + dragging: Option<Pane>, + resizing: Option<Axis>, + layout: Layout<'_>, + cursor_position: Point, + ) -> Self::Output { + let pane_cursor_position = if dragging.is_some() { + // TODO: Remove once cursor availability is encoded in the type + // system + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + let mut mouse_interaction = mouse::Interaction::default(); + let mut dragged_pane = None; + + let mut panes: Vec<_> = content + .iter() + .zip(layout.children()) + .enumerate() + .map(|(i, ((id, pane), layout))| { + let (primitive, new_mouse_interaction) = + pane.draw(self, defaults, layout, pane_cursor_position); + + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + + if Some(*id) == dragging { + dragged_pane = Some((i, layout)); + } + + primitive + }) + .collect(); + + let primitives = if let Some((index, layout)) = dragged_pane { + let pane = panes.remove(index); + let bounds = layout.bounds(); + + // TODO: Fix once proper layering is implemented. + // This is a pretty hacky way to achieve layering. + let clip = Primitive::Clip { + bounds: Rectangle { + x: cursor_position.x - bounds.width / 2.0, + y: cursor_position.y - bounds.height / 2.0, + width: bounds.width + 0.5, + height: bounds.height + 0.5, + }, + offset: Vector::new(0, 0), + content: Box::new(Primitive::Translate { + translation: Vector::new( + cursor_position.x - bounds.x - bounds.width / 2.0, + cursor_position.y - bounds.y - bounds.height / 2.0, + ), + content: Box::new(pane), + }), + }; + + panes.push(clip); + + panes + } else { + panes + }; + + ( + Primitive::Group { primitives }, + if dragging.is_some() { + mouse::Interaction::Grabbing + } else if let Some(axis) = resizing { + match axis { + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, + } + } else { + mouse_interaction + }, + ) + } +} diff --git a/glow/src/renderer/widget/progress_bar.rs b/glow/src/renderer/widget/progress_bar.rs new file mode 100644 index 00000000..2baeeb14 --- /dev/null +++ b/glow/src/renderer/widget/progress_bar.rs @@ -0,0 +1,54 @@ +use crate::{progress_bar::StyleSheet, Primitive, Renderer}; +use iced_native::{mouse, progress_bar, Color, Rectangle}; + +impl progress_bar::Renderer for Renderer { + type Style = Box<dyn StyleSheet>; + + const DEFAULT_HEIGHT: u16 = 30; + + fn draw( + &self, + bounds: Rectangle, + range: std::ops::RangeInclusive<f32>, + value: f32, + style_sheet: &Self::Style, + ) -> Self::Output { + let style = style_sheet.style(); + + let (range_start, range_end) = range.into_inner(); + let active_progress_width = bounds.width + * ((value - range_start) / (range_end - range_start).max(1.0)); + + let background = Primitive::Group { + primitives: vec![Primitive::Quad { + bounds: Rectangle { ..bounds }, + background: style.background, + border_radius: style.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }], + }; + + ( + if active_progress_width > 0.0 { + let bar = Primitive::Quad { + bounds: Rectangle { + width: active_progress_width, + ..bounds + }, + background: style.bar, + border_radius: style.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }; + + Primitive::Group { + primitives: vec![background, bar], + } + } else { + background + }, + mouse::Interaction::default(), + ) + } +} diff --git a/glow/src/renderer/widget/radio.rs b/glow/src/renderer/widget/radio.rs new file mode 100644 index 00000000..cee0deb6 --- /dev/null +++ b/glow/src/renderer/widget/radio.rs @@ -0,0 +1,63 @@ +use crate::{radio::StyleSheet, Primitive, Renderer}; +use iced_native::{mouse, radio, Background, Color, Rectangle}; + +const SIZE: f32 = 28.0; +const DOT_SIZE: f32 = SIZE / 2.0; + +impl radio::Renderer for Renderer { + type Style = Box<dyn StyleSheet>; + + const DEFAULT_SIZE: u16 = SIZE as u16; + const DEFAULT_SPACING: u16 = 15; + + fn draw( + &mut self, + bounds: Rectangle, + is_selected: bool, + is_mouse_over: bool, + (label, _): Self::Output, + style_sheet: &Self::Style, + ) -> Self::Output { + let style = if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let radio = Primitive::Quad { + bounds, + background: style.background, + border_radius: (SIZE / 2.0) as u16, + border_width: style.border_width, + border_color: style.border_color, + }; + + ( + Primitive::Group { + primitives: if is_selected { + let radio_circle = Primitive::Quad { + bounds: Rectangle { + x: bounds.x + DOT_SIZE / 2.0, + y: bounds.y + DOT_SIZE / 2.0, + width: bounds.width - DOT_SIZE, + height: bounds.height - DOT_SIZE, + }, + background: Background::Color(style.dot_color), + border_radius: (DOT_SIZE / 2.0) as u16, + border_width: 0, + border_color: Color::TRANSPARENT, + }; + + vec![radio, radio_circle, label] + } else { + vec![radio, label] + }, + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/glow/src/renderer/widget/row.rs b/glow/src/renderer/widget/row.rs new file mode 100644 index 00000000..d0b7ef09 --- /dev/null +++ b/glow/src/renderer/widget/row.rs @@ -0,0 +1,34 @@ +use crate::{Primitive, Renderer}; +use iced_native::{mouse, row, Element, Layout, Point}; + +impl row::Renderer for Renderer { + fn draw<Message>( + &mut self, + defaults: &Self::Defaults, + children: &[Element<'_, Message, Self>], + layout: Layout<'_>, + cursor_position: Point, + ) -> Self::Output { + let mut mouse_interaction = mouse::Interaction::default(); + + ( + Primitive::Group { + primitives: children + .iter() + .zip(layout.children()) + .map(|(child, layout)| { + let (primitive, new_mouse_interaction) = + child.draw(self, defaults, layout, cursor_position); + + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + + primitive + }) + .collect(), + }, + mouse_interaction, + ) + } +} diff --git a/glow/src/renderer/widget/scrollable.rs b/glow/src/renderer/widget/scrollable.rs new file mode 100644 index 00000000..8a400b82 --- /dev/null +++ b/glow/src/renderer/widget/scrollable.rs @@ -0,0 +1,125 @@ +use crate::{Primitive, Renderer}; +use iced_native::{mouse, scrollable, Background, Color, Rectangle, Vector}; + +const SCROLLBAR_WIDTH: u16 = 10; +const SCROLLBAR_MARGIN: u16 = 2; + +impl scrollable::Renderer for Renderer { + type Style = Box<dyn iced_style::scrollable::StyleSheet>; + + fn scrollbar( + &self, + bounds: Rectangle, + content_bounds: Rectangle, + offset: u32, + ) -> Option<scrollable::Scrollbar> { + if content_bounds.height > bounds.height { + let scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), + y: bounds.y, + width: f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), + height: bounds.height, + }; + + let ratio = bounds.height / content_bounds.height; + let scrollbar_height = bounds.height * ratio; + let y_offset = offset as f32 * ratio; + + let scroller_bounds = Rectangle { + x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN), + y: scrollbar_bounds.y + y_offset, + width: scrollbar_bounds.width - f32::from(2 * SCROLLBAR_MARGIN), + height: scrollbar_height, + }; + + Some(scrollable::Scrollbar { + bounds: scrollbar_bounds, + scroller: scrollable::Scroller { + bounds: scroller_bounds, + }, + }) + } else { + None + } + } + + fn draw( + &mut self, + state: &scrollable::State, + bounds: Rectangle, + _content_bounds: Rectangle, + is_mouse_over: bool, + is_mouse_over_scrollbar: bool, + scrollbar: Option<scrollable::Scrollbar>, + offset: u32, + style_sheet: &Self::Style, + (content, mouse_interaction): Self::Output, + ) -> Self::Output { + ( + if let Some(scrollbar) = scrollbar { + let clip = Primitive::Clip { + bounds, + offset: Vector::new(0, offset), + content: Box::new(content), + }; + + let style = if state.is_scroller_grabbed() { + style_sheet.dragging() + } else if is_mouse_over_scrollbar { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let is_scrollbar_visible = + style.background.is_some() || style.border_width > 0; + + let scroller = if is_mouse_over + || state.is_scroller_grabbed() + || is_scrollbar_visible + { + Primitive::Quad { + bounds: scrollbar.scroller.bounds, + background: Background::Color(style.scroller.color), + border_radius: style.scroller.border_radius, + border_width: style.scroller.border_width, + border_color: style.scroller.border_color, + } + } else { + Primitive::None + }; + + let scrollbar = if is_scrollbar_visible { + Primitive::Quad { + bounds: Rectangle { + x: scrollbar.bounds.x + f32::from(SCROLLBAR_MARGIN), + width: scrollbar.bounds.width + - f32::from(2 * SCROLLBAR_MARGIN), + ..scrollbar.bounds + }, + background: style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + } + } else { + Primitive::None + }; + + Primitive::Group { + primitives: vec![clip, scrollbar, scroller], + } + } else { + content + }, + if is_mouse_over_scrollbar || state.is_scroller_grabbed() { + mouse::Interaction::Idle + } else { + mouse_interaction + }, + ) + } +} diff --git a/glow/src/renderer/widget/slider.rs b/glow/src/renderer/widget/slider.rs new file mode 100644 index 00000000..220feace --- /dev/null +++ b/glow/src/renderer/widget/slider.rs @@ -0,0 +1,106 @@ +use crate::{ + slider::{HandleShape, StyleSheet}, + Primitive, Renderer, +}; +use iced_native::{mouse, slider, Background, Color, Point, Rectangle}; + +const HANDLE_HEIGHT: f32 = 22.0; + +impl slider::Renderer for Renderer { + type Style = Box<dyn StyleSheet>; + + fn height(&self) -> u32 { + 30 + } + + fn draw( + &mut self, + bounds: Rectangle, + cursor_position: Point, + range: std::ops::RangeInclusive<f32>, + value: f32, + is_dragging: bool, + style_sheet: &Self::Style, + ) -> Self::Output { + let is_mouse_over = bounds.contains(cursor_position); + + let style = if is_dragging { + style_sheet.dragging() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let rail_y = bounds.y + (bounds.height / 2.0).round(); + + let (rail_top, rail_bottom) = ( + Primitive::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y, + width: bounds.width, + height: 2.0, + }, + background: Background::Color(style.rail_colors.0), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + Primitive::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y + 2.0, + width: bounds.width, + height: 2.0, + }, + background: Background::Color(style.rail_colors.1), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + ); + + let (range_start, range_end) = range.into_inner(); + + let (handle_width, handle_height, handle_border_radius) = + match style.handle.shape { + HandleShape::Circle { radius } => { + (f32::from(radius * 2), f32::from(radius * 2), radius) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), HANDLE_HEIGHT, border_radius), + }; + + let handle_offset = (bounds.width - handle_width) + * ((value - range_start) / (range_end - range_start).max(1.0)); + + let handle = Primitive::Quad { + bounds: Rectangle { + x: bounds.x + handle_offset.round(), + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, + }, + background: Background::Color(style.handle.color), + border_radius: handle_border_radius, + border_width: style.handle.border_width, + border_color: style.handle.border_color, + }; + + ( + Primitive::Group { + primitives: vec![rail_top, rail_bottom, handle], + }, + if is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/glow/src/renderer/widget/space.rs b/glow/src/renderer/widget/space.rs new file mode 100644 index 00000000..225f7e6c --- /dev/null +++ b/glow/src/renderer/widget/space.rs @@ -0,0 +1,8 @@ +use crate::{Primitive, Renderer}; +use iced_native::{mouse, space, Rectangle}; + +impl space::Renderer for Renderer { + fn draw(&mut self, _bounds: Rectangle) -> Self::Output { + (Primitive::None, mouse::Interaction::default()) + } +} diff --git a/glow/src/renderer/widget/svg.rs b/glow/src/renderer/widget/svg.rs new file mode 100644 index 00000000..f6d6d0ba --- /dev/null +++ b/glow/src/renderer/widget/svg.rs @@ -0,0 +1,22 @@ +use crate::{Primitive, Renderer}; +use iced_native::{mouse, svg, Layout}; + +impl svg::Renderer for Renderer { + fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { + self.image_pipeline.viewport_dimensions(handle) + } + + fn draw( + &mut self, + handle: svg::Handle, + layout: Layout<'_>, + ) -> Self::Output { + ( + Primitive::Svg { + handle, + bounds: layout.bounds(), + }, + mouse::Interaction::default(), + ) + } +} diff --git a/glow/src/renderer/widget/text.rs b/glow/src/renderer/widget/text.rs new file mode 100644 index 00000000..4605ed06 --- /dev/null +++ b/glow/src/renderer/widget/text.rs @@ -0,0 +1,61 @@ +use crate::{Primitive, Renderer}; +use iced_native::{ + mouse, text, Color, Font, HorizontalAlignment, Rectangle, Size, + VerticalAlignment, +}; + +use std::f32; + +impl text::Renderer for Renderer { + type Font = Font; + + const DEFAULT_SIZE: u16 = 20; + + fn measure( + &self, + content: &str, + size: u16, + font: Font, + bounds: Size, + ) -> (f32, f32) { + self.text_pipeline + .measure(content, f32::from(size), font, bounds) + } + + fn draw( + &mut self, + defaults: &Self::Defaults, + bounds: Rectangle, + content: &str, + size: u16, + font: Font, + color: Option<Color>, + horizontal_alignment: HorizontalAlignment, + vertical_alignment: VerticalAlignment, + ) -> Self::Output { + let x = match horizontal_alignment { + iced_native::HorizontalAlignment::Left => bounds.x, + iced_native::HorizontalAlignment::Center => bounds.center_x(), + iced_native::HorizontalAlignment::Right => bounds.x + bounds.width, + }; + + let y = match vertical_alignment { + iced_native::VerticalAlignment::Top => bounds.y, + iced_native::VerticalAlignment::Center => bounds.center_y(), + iced_native::VerticalAlignment::Bottom => bounds.y + bounds.height, + }; + + ( + Primitive::Text { + content: content.to_string(), + size: f32::from(size), + bounds: Rectangle { x, y, ..bounds }, + color: color.unwrap_or(defaults.text.color), + font, + horizontal_alignment, + vertical_alignment, + }, + mouse::Interaction::default(), + ) + } +} diff --git a/glow/src/renderer/widget/text_input.rs b/glow/src/renderer/widget/text_input.rs new file mode 100644 index 00000000..57be6692 --- /dev/null +++ b/glow/src/renderer/widget/text_input.rs @@ -0,0 +1,261 @@ +use crate::{text_input::StyleSheet, Primitive, Renderer}; + +use iced_native::{ + mouse, + text_input::{self, cursor}, + Background, Color, Font, HorizontalAlignment, Point, Rectangle, Size, + Vector, VerticalAlignment, +}; +use std::f32; + +impl text_input::Renderer for Renderer { + type Style = Box<dyn StyleSheet>; + + fn default_size(&self) -> u16 { + // TODO: Make this configurable + 20 + } + + fn measure_value(&self, value: &str, size: u16, font: Font) -> f32 { + let (mut width, _) = self.text_pipeline.measure( + value, + f32::from(size), + font, + Size::INFINITY, + ); + + let spaces_around = value.len() - value.trim().len(); + + if spaces_around > 0 { + let space_width = self.text_pipeline.space_width(size as f32); + width += spaces_around as f32 * space_width; + } + + width + } + + fn offset( + &self, + text_bounds: Rectangle, + font: Font, + size: u16, + value: &text_input::Value, + state: &text_input::State, + ) -> f32 { + if state.is_focused() { + let cursor = state.cursor(); + + let focus_position = match cursor.state(value) { + cursor::State::Index(i) => i, + cursor::State::Selection { end, .. } => end, + }; + + let (_, offset) = measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + focus_position, + font, + ); + + offset + } else { + 0.0 + } + } + + fn draw( + &mut self, + bounds: Rectangle, + text_bounds: Rectangle, + cursor_position: Point, + font: Font, + size: u16, + placeholder: &str, + value: &text_input::Value, + state: &text_input::State, + style_sheet: &Self::Style, + ) -> Self::Output { + let is_mouse_over = bounds.contains(cursor_position); + + let style = if state.is_focused() { + style_sheet.focused() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let input = Primitive::Quad { + bounds, + background: style.background, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }; + + let text = value.to_string(); + + let text_value = Primitive::Text { + content: if text.is_empty() { + placeholder.to_string() + } else { + text.clone() + }, + color: if text.is_empty() { + style_sheet.placeholder_color() + } else { + style_sheet.value_color() + }, + font, + bounds: Rectangle { + y: text_bounds.center_y(), + width: f32::INFINITY, + ..text_bounds + }, + size: f32::from(size), + horizontal_alignment: HorizontalAlignment::Left, + vertical_alignment: VerticalAlignment::Center, + }; + + let (contents_primitive, offset) = if state.is_focused() { + let cursor = state.cursor(); + + let (cursor_primitive, offset) = match cursor.state(value) { + cursor::State::Index(position) => { + let (text_value_width, offset) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + position, + font, + ); + + ( + Primitive::Quad { + bounds: Rectangle { + x: text_bounds.x + text_value_width, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + background: Background::Color( + style_sheet.value_color(), + ), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + offset, + ) + } + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); + + let (left_position, left_offset) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + left, + font, + ); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + right, + font, + ); + + let width = right_position - left_position; + + ( + Primitive::Quad { + bounds: Rectangle { + x: text_bounds.x + left_position, + y: text_bounds.y, + width, + height: text_bounds.height, + }, + background: Background::Color( + style_sheet.selection_color(), + ), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + if end == right { + right_offset + } else { + left_offset + }, + ) + } + }; + + ( + Primitive::Group { + primitives: vec![cursor_primitive, text_value], + }, + Vector::new(offset as u32, 0), + ) + } else { + (text_value, Vector::new(0, 0)) + }; + + let text_width = self.measure_value( + if text.is_empty() { placeholder } else { &text }, + size, + font, + ); + + let contents = if text_width > text_bounds.width { + Primitive::Clip { + bounds: text_bounds, + offset, + content: Box::new(contents_primitive), + } + } else { + contents_primitive + }; + + ( + Primitive::Group { + primitives: vec![input, contents], + }, + if is_mouse_over { + mouse::Interaction::Text + } else { + mouse::Interaction::default() + }, + ) + } +} + +fn measure_cursor_and_scroll_offset( + renderer: &Renderer, + text_bounds: Rectangle, + value: &text_input::Value, + size: u16, + cursor_index: usize, + font: Font, +) -> (f32, f32) { + use iced_native::text_input::Renderer; + + let text_before_cursor = value.until(cursor_index).to_string(); + + let text_value_width = + renderer.measure_value(&text_before_cursor, size, font); + let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); + + (text_value_width, offset) +} diff --git a/glow/src/settings.rs b/glow/src/settings.rs new file mode 100644 index 00000000..bffc867e --- /dev/null +++ b/glow/src/settings.rs @@ -0,0 +1,50 @@ +//! Configure a [`Renderer`]. +//! +//! [`Renderer`]: struct.Renderer.html + +/// 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, + } + } +} + +/// An antialiasing strategy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Antialiasing { + /// Multisample AA with 2 samples + MSAAx2, + /// Multisample AA with 4 samples + MSAAx4, + /// Multisample AA with 8 samples + MSAAx8, + /// Multisample AA with 16 samples + MSAAx16, +} + +impl Antialiasing { + pub(crate) fn sample_count(self) -> u32 { + match self { + Antialiasing::MSAAx2 => 2, + Antialiasing::MSAAx4 => 4, + Antialiasing::MSAAx8 => 8, + Antialiasing::MSAAx16 => 16, + } + } +} diff --git a/glow/src/shader/quad.frag b/glow/src/shader/quad.frag new file mode 100644 index 00000000..d9e74664 --- /dev/null +++ b/glow/src/shader/quad.frag @@ -0,0 +1,67 @@ +#version 450 + +layout(origin_upper_left) in vec4 gl_FragCoord; +layout(location = 0) in vec4 v_Color; +layout(location = 1) in vec4 v_BorderColor; +layout(location = 2) in vec2 v_Pos; +layout(location = 3) in vec2 v_Scale; +layout(location = 4) in float v_BorderRadius; +layout(location = 5) in float v_BorderWidth; + +layout(location = 0) 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), + max(max(top_left_distance.y, bottom_right_distance.y), 0) + ); + + return sqrt(distance.x * distance.x + distance.y * distance.y); +} + +void main() { + vec4 mixed_color; + + // TODO: Remove branching (?) + if(v_BorderWidth > 0) { + float internal_border = max(v_BorderRadius - v_BorderWidth, 0); + + float internal_distance = distance( + gl_FragCoord.xy, + 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( + gl_FragCoord.xy, + v_Pos, + v_Scale, + v_BorderRadius + ); + + float radius_alpha = + 1.0 - smoothstep(max(v_BorderRadius - 0.5, 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..2d2ebc3d --- /dev/null +++ b/glow/src/shader/quad.vert @@ -0,0 +1,47 @@ +#version 450 + +layout(location = 0) uniform mat4 u_Transform; +layout(location = 1) 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; + +layout(location = 0) out vec4 o_Color; +layout(location = 1) out vec4 o_BorderColor; +layout(location = 2) out vec2 o_Pos; +layout(location = 3) out vec2 o_Scale; +layout(location = 4) out float o_BorderRadius; +layout(location = 5) out float o_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 v_Pos = positions[gl_VertexID]; + vec2 p_Pos = i_Pos * u_Scale; + vec2 p_Scale = i_Scale * u_Scale; + + mat4 i_Transform = mat4( + vec4(p_Scale.x + 1.0, 0.0, 0.0, 0.0), + vec4(0.0, p_Scale.y + 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(p_Pos - vec2(0.5, 0.5), 0.0, 1.0) + ); + + o_Color = i_Color; + o_BorderColor = i_BorderColor; + o_Pos = p_Pos; + o_Scale = p_Scale; + o_BorderRadius = i_BorderRadius * u_Scale; + o_BorderWidth = i_BorderWidth * u_Scale; + + gl_Position = u_Transform * i_Transform * vec4(v_Pos, 0.0, 1.0); +} diff --git a/glow/src/text.rs b/glow/src/text.rs new file mode 100644 index 00000000..bda619fc --- /dev/null +++ b/glow/src/text.rs @@ -0,0 +1,180 @@ +mod font; + +use crate::Transformation; + +use std::{cell::RefCell, collections::HashMap}; + +pub const BUILTIN_ICONS: iced_native::Font = iced_native::Font::External { + name: "iced_glow icons", + bytes: include_bytes!("text/icons.ttf"), +}; + +pub const CHECKMARK_ICON: char = '\u{F00C}'; + +const FALLBACK_FONT: &[u8] = + include_bytes!("../../wgpu/fonts/Lato-Regular.ttf"); + +#[derive(Debug)] +pub struct Pipeline { + draw_brush: RefCell<glow_glyph::GlyphBrush<'static>>, + draw_font_map: RefCell<HashMap<String, glow_glyph::FontId>>, + + measure_brush: RefCell<glyph_brush::GlyphBrush<'static, ()>>, +} + +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(|_| FALLBACK_FONT.to_vec()) + }); + + let load_glyph_brush = |font: Vec<u8>| { + let builder = + glow_glyph::GlyphBrushBuilder::using_fonts_bytes(vec![ + font.clone() + ])?; + + Ok(( + builder, + glyph_brush::GlyphBrushBuilder::using_font_bytes(font).build(), + )) + }; + + let (brush_builder, measure_brush) = load_glyph_brush(default_font) + .unwrap_or_else(|_: glow_glyph::rusttype::Error| { + log::warn!("System font failed to load. Falling back to embedded font..."); + + load_glyph_brush(FALLBACK_FONT.to_vec()).expect("Load fallback font") + }); + + let draw_brush = + brush_builder.initial_cache_size((2048, 2048)).build(gl); + + Pipeline { + draw_brush: RefCell::new(draw_brush), + draw_font_map: RefCell::new(HashMap::new()), + + measure_brush: RefCell::new(measure_brush), + } + } + + pub fn overlay_font(&self) -> glow_glyph::FontId { + glow_glyph::FontId(0) + } + + 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 { + text: content, + scale: glow_glyph::Scale { x: size, y: size }, + bounds: (bounds.width, bounds.height), + font_id: glow_glyph::FontId(font_id), + ..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 space_width(&self, size: f32) -> f32 { + use glow_glyph::GlyphCruncher; + + let glyph_brush = self.measure_brush.borrow(); + + // TODO: Select appropriate font + let font = &glyph_brush.fonts()[0]; + + font.glyph(' ') + .scaled(glow_glyph::Scale { x: size, y: size }) + .h_metrics() + .advance_width + } + + pub fn clear_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; + } + + // TODO: Find a way to share font data + let _ = self.measure_brush.borrow_mut().add_font_bytes(bytes); + + let font_id = + self.draw_brush.borrow_mut().add_font_bytes(bytes); + + let _ = self + .draw_font_map + .borrow_mut() + .insert(String::from(name), font_id); + + font_id + } + } + } +} diff --git a/glow/src/text/font.rs b/glow/src/text/font.rs new file mode 100644 index 00000000..7346ccdb --- /dev/null +++ b/glow/src/text/font.rs @@ -0,0 +1,37 @@ +pub use font_kit::{ + error::SelectionError as LoadError, family_name::FamilyName as Family, +}; + +pub struct Source { + raw: font_kit::source::SystemSource, +} + +impl Source { + pub fn new() -> Self { + Source { + raw: font_kit::source::SystemSource::new(), + } + } + + pub fn load(&self, families: &[Family]) -> Result<Vec<u8>, LoadError> { + let font = self.raw.select_best_match( + families, + &font_kit::properties::Properties::default(), + )?; + + match font { + font_kit::handle::Handle::Path { path, .. } => { + use std::io::Read; + + let mut buf = Vec::new(); + let mut reader = std::fs::File::open(path).expect("Read font"); + let _ = reader.read_to_end(&mut buf); + + Ok(buf) + } + font_kit::handle::Handle::Memory { bytes, .. } => { + Ok(bytes.as_ref().clone()) + } + } + } +} diff --git a/glow/src/text/icons.ttf b/glow/src/text/icons.ttf Binary files differnew file mode 100644 index 00000000..1c832f86 --- /dev/null +++ b/glow/src/text/icons.ttf diff --git a/glow/src/transformation.rs b/glow/src/transformation.rs new file mode 100644 index 00000000..ff3b1d00 --- /dev/null +++ b/glow/src/transformation.rs @@ -0,0 +1,54 @@ +use glam::{Mat4, Vec3, Vec4}; +use std::ops::Mul; + +/// A 2D transformation matrix. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Transformation(Mat4); + +impl Transformation { + /// Get the identity transformation. + pub fn identity() -> Transformation { + Transformation(Mat4::identity()) + } + + /// Creates an orthographic projection. + #[rustfmt::skip] + pub fn orthographic(width: u32, height: u32) -> Transformation { + Transformation(Mat4::from_cols( + Vec4::new(2.0 / width as f32, 0.0, 0.0, 0.0), + Vec4::new(0.0, -2.0 / height as f32, 0.0, 0.0), + Vec4::new(0.0, 0.0, -1.0, 0.0), + Vec4::new(-1.0, 1.0, 0.0, 1.0) + )) + } + + /// Creates a translate transformation. + pub fn translate(x: f32, y: f32) -> Transformation { + Transformation(Mat4::from_translation(Vec3::new(x, y, 0.0))) + } + + /// Creates a scale transformation. + pub fn scale(x: f32, y: f32) -> Transformation { + Transformation(Mat4::from_scale(Vec3::new(x, y, 1.0))) + } +} + +impl Mul for Transformation { + type Output = Self; + + fn mul(self, rhs: Self) -> Self { + Transformation(self.0 * rhs.0) + } +} + +impl AsRef<[f32; 16]> for Transformation { + fn as_ref(&self) -> &[f32; 16] { + self.0.as_ref() + } +} + +impl From<Transformation> for [f32; 16] { + fn from(t: Transformation) -> [f32; 16] { + *t.as_ref() + } +} diff --git a/glow/src/triangle.rs b/glow/src/triangle.rs new file mode 100644 index 00000000..f71022d0 --- /dev/null +++ b/glow/src/triangle.rs @@ -0,0 +1,84 @@ +//! Draw meshes of triangles. +use crate::{settings, Transformation}; +use iced_native::{Rectangle, Vector}; +use std::mem; + +const UNIFORM_BUFFER_SIZE: usize = 100; +const VERTEX_BUFFER_SIZE: usize = 10_000; +const INDEX_BUFFER_SIZE: usize = 10_000; + +#[derive(Debug)] +pub(crate) struct Pipeline {} + +impl Pipeline { + pub fn new( + gl: &glow::Context, + antialiasing: Option<settings::Antialiasing>, + ) -> Pipeline { + Pipeline {} + } + + pub fn draw( + &mut self, + gl: &glow::Context, + target_width: u32, + target_height: u32, + transformation: Transformation, + scale_factor: f32, + meshes: &[(Vector, Rectangle<u32>, &Mesh2D)], + ) { + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct Uniforms { + transform: [f32; 16], + // We need to align this to 256 bytes to please `wgpu`... + // TODO: Be smarter and stop wasting memory! + _padding_a: [f32; 32], + _padding_b: [f32; 16], +} + +impl Default for Uniforms { + fn default() -> Self { + Self { + transform: *Transformation::identity().as_ref(), + _padding_a: [0.0; 32], + _padding_b: [0.0; 16], + } + } +} + +impl From<Transformation> for Uniforms { + fn from(transformation: Transformation) -> Uniforms { + Self { + transform: transformation.into(), + _padding_a: [0.0; 32], + _padding_b: [0.0; 16], + } + } +} + +/// A two-dimensional vertex with some color in __linear__ RGBA. +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct Vertex2D { + /// The vertex position + pub position: [f32; 2], + /// The vertex color in __linear__ RGBA. + pub color: [f32; 4], +} + +/// A set of [`Vertex2D`] and indices representing a list of triangles. +/// +/// [`Vertex2D`]: struct.Vertex2D.html +#[derive(Clone, Debug)] +pub struct Mesh2D { + /// The vertices of the mesh + pub vertices: Vec<Vertex2D>, + /// The list of vertex indices that defines the triangles of the mesh. + /// + /// Therefore, this list should always have a length that is a multiple of 3. + pub indices: Vec<u32>, +} diff --git a/glow/src/viewport.rs b/glow/src/viewport.rs new file mode 100644 index 00000000..c1afee87 --- /dev/null +++ b/glow/src/viewport.rs @@ -0,0 +1,33 @@ +use crate::Transformation; + +/// A viewing region for displaying computer graphics. +#[derive(Debug)] +pub struct Viewport { + width: u32, + height: u32, + transformation: Transformation, +} + +impl Viewport { + /// Creates a new [`Viewport`] with the given dimensions. + pub fn new(width: u32, height: u32) -> Viewport { + Viewport { + width, + height, + transformation: Transformation::orthographic(width, height), + } + } + + pub fn height(&self) -> u32 { + self.height + } + + /// Returns the dimensions of the [`Viewport`]. + pub fn dimensions(&self) -> (u32, u32) { + (self.width, self.height) + } + + pub(crate) fn transformation(&self) -> Transformation { + self.transformation + } +} diff --git a/glow/src/widget.rs b/glow/src/widget.rs new file mode 100644 index 00000000..16e7ca88 --- /dev/null +++ b/glow/src/widget.rs @@ -0,0 +1,44 @@ +//! 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; + +pub use iced_native::{Image, Space, Text}; + +pub type Column<'a, Message> = iced_native::Column<'a, Message, Renderer>; +pub type Row<'a, Message> = iced_native::Row<'a, Message, Renderer>; diff --git a/glow/src/widget/button.rs b/glow/src/widget/button.rs new file mode 100644 index 00000000..b738c55e --- /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_native::button::State; +pub use iced_style::button::{Style, StyleSheet}; + +/// 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..325f90ce --- /dev/null +++ b/glow/src/widget/canvas.rs @@ -0,0 +1,201 @@ +//! 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 +use crate::{Defaults, Primitive, Renderer}; + +use iced_native::{ + layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, Widget, +}; +use std::hash::Hash; + +pub mod layer; +pub mod path; + +mod drawable; +mod fill; +mod frame; +mod stroke; +mod text; + +pub use drawable::Drawable; +pub use fill::Fill; +pub use frame::Frame; +pub use layer::Layer; +pub use path::Path; +pub use stroke::{LineCap, LineJoin, Stroke}; +pub use text::Text; + +/// A widget capable of drawing 2D graphics. +/// +/// A [`Canvas`] may contain multiple layers. A [`Layer`] is drawn using the +/// painter's algorithm. In other words, layers will be drawn on top of each +/// other in the same order they are pushed into the [`Canvas`]. +/// +/// [`Canvas`]: struct.Canvas.html +/// [`Layer`]: layer/trait.Layer.html +/// +/// # Examples +/// The repository has a couple of [examples] showcasing how to use a +/// [`Canvas`]: +/// +/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock +/// and its hands to display the current time. +/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget +/// and showcasing how to compose different transforms. +/// +/// [examples]: https://github.com/hecrj/iced/tree/0.1/examples +/// [`clock`]: https://github.com/hecrj/iced/tree/0.1/examples/clock +/// [`solar_system`]: https://github.com/hecrj/iced/tree/0.1/examples/solar_system +/// +/// ## Drawing a simple circle +/// If you want to get a quick overview, here's how we can draw a simple circle: +/// +/// ```no_run +/// # mod iced { +/// # pub use iced_wgpu::canvas; +/// # pub use iced_native::Color; +/// # } +/// use iced::canvas::{self, layer, Canvas, Drawable, Fill, Frame, Path}; +/// use iced::Color; +/// +/// // First, we define the data we need for drawing +/// #[derive(Debug)] +/// struct Circle { +/// radius: f32, +/// } +/// +/// // Then, we implement the `Drawable` trait +/// impl Drawable for Circle { +/// fn draw(&self, frame: &mut Frame) { +/// // We create a `Path` representing a simple circle +/// let circle = Path::new(|p| p.circle(frame.center(), self.radius)); +/// +/// // And fill it with some color +/// frame.fill(&circle, Fill::Color(Color::BLACK)); +/// } +/// } +/// +/// // We can use a `Cache` to avoid unnecessary re-tessellation +/// let cache: layer::Cache<Circle> = layer::Cache::new(); +/// +/// // Finally, we simply provide the data to our `Cache` and push the resulting +/// // layer into a `Canvas` +/// let canvas = Canvas::new() +/// .push(cache.with(&Circle { radius: 50.0 })); +/// ``` +#[derive(Debug)] +pub struct Canvas<'a> { + width: Length, + height: Length, + layers: Vec<Box<dyn Layer + 'a>>, +} + +impl<'a> Canvas<'a> { + const DEFAULT_SIZE: u16 = 100; + + /// Creates a new [`Canvas`] with no layers. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn new() -> Self { + Canvas { + width: Length::Units(Self::DEFAULT_SIZE), + height: Length::Units(Self::DEFAULT_SIZE), + layers: Vec::new(), + } + } + + /// Sets the width of the [`Canvas`]. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Canvas`]. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Adds a [`Layer`] to the [`Canvas`]. + /// + /// It will be drawn on top of previous layers. + /// + /// [`Layer`]: layer/trait.Layer.html + /// [`Canvas`]: struct.Canvas.html + pub fn push(mut self, layer: impl Layer + 'a) -> Self { + self.layers.push(Box::new(layer)); + self + } +} + +impl<'a, Message> Widget<Message, Renderer> for Canvas<'a> { + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn draw( + &self, + _renderer: &mut Renderer, + _defaults: &Defaults, + layout: Layout<'_>, + _cursor_position: Point, + ) -> (Primitive, MouseCursor) { + let bounds = layout.bounds(); + let origin = Point::new(bounds.x, bounds.y); + let size = Size::new(bounds.width, bounds.height); + + ( + Primitive::Group { + primitives: self + .layers + .iter() + .map(|layer| Primitive::Cached { + origin, + cache: layer.draw(size), + }) + .collect(), + }, + MouseCursor::Idle, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + std::any::TypeId::of::<Canvas<'static>>().hash(state); + + self.width.hash(state); + self.height.hash(state); + } +} + +impl<'a, Message> From<Canvas<'a>> for Element<'a, Message, Renderer> +where + Message: 'static, +{ + fn from(canvas: Canvas<'a>) -> Element<'a, Message, Renderer> { + Element::new(canvas) + } +} diff --git a/glow/src/widget/checkbox.rs b/glow/src/widget/checkbox.rs new file mode 100644 index 00000000..da0d7a84 --- /dev/null +++ b/glow/src/widget/checkbox.rs @@ -0,0 +1,9 @@ +//! Show toggle controls using checkboxes. +use crate::Renderer; + +pub use iced_style::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..9a93a246 --- /dev/null +++ b/glow/src/widget/container.rs @@ -0,0 +1,10 @@ +//! Decorate content and apply alignment. +use crate::Renderer; + +pub use iced_style::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..34450b5e --- /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_style::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..6e5cf042 --- /dev/null +++ b/glow/src/widget/radio.rs @@ -0,0 +1,10 @@ +//! Create choices using radio buttons. +use crate::Renderer; + +pub use iced_style::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..1d236105 --- /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_native::scrollable::State; +pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; + +/// 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..4e47978f --- /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_native::slider::State; +pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; + +/// 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..260fe3a6 --- /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_native::text_input::State; +pub use iced_style::text_input::{Style, StyleSheet}; + +/// 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..b7adad82 --- /dev/null +++ b/glow/src/window.rs @@ -0,0 +1,6 @@ +//! Display rendering results on windows. +mod backend; +mod swap_chain; + +pub use backend::Backend; +pub use swap_chain::SwapChain; diff --git a/glow/src/window/backend.rs b/glow/src/window/backend.rs new file mode 100644 index 00000000..05f988f7 --- /dev/null +++ b/glow/src/window/backend.rs @@ -0,0 +1,183 @@ +use crate::{Renderer, Settings, Viewport}; + +use glow::HasContext; +use iced_native::mouse; +use raw_window_handle::HasRawWindowHandle; + +/// A window graphics backend for iced powered by `glow`. +#[allow(missing_debug_implementations)] +pub struct Backend { + connection: surfman::Connection, + device: surfman::Device, + gl_context: surfman::Context, + gl: Option<glow::Context>, +} + +impl iced_native::window::Backend for Backend { + type Settings = Settings; + type Renderer = Renderer; + type Surface = (); + type SwapChain = Viewport; + + fn new(settings: Self::Settings) -> Backend { + let connection = surfman::Connection::new().expect("Create connection"); + + let adapter = connection + .create_hardware_adapter() + .expect("Create adapter"); + + let mut device = + connection.create_device(&adapter).expect("Create device"); + + let context_descriptor = device + .create_context_descriptor(&surfman::ContextAttributes { + version: surfman::GLVersion::new(3, 0), + flags: surfman::ContextAttributeFlags::empty(), + }) + .expect("Create context descriptor"); + + let gl_context = device + .create_context(&context_descriptor) + .expect("Create context"); + + Backend { + connection, + device, + gl_context, + gl: None, + } + } + + fn create_renderer(&mut self, settings: Settings) -> Renderer { + self.device + .make_context_current(&self.gl_context) + .expect("Make context current"); + + Renderer::new(self.gl.as_ref().unwrap(), settings) + } + + fn create_surface<W: HasRawWindowHandle>( + &mut self, + window: &W, + ) -> Self::Surface { + let native_widget = self + .connection + .create_native_widget_from_rwh(window.raw_window_handle()) + .expect("Create widget"); + + let surface = self + .device + .create_surface( + &self.gl_context, + surfman::SurfaceAccess::GPUOnly, + surfman::SurfaceType::Widget { native_widget }, + ) + .expect("Create surface"); + + let surfman::SurfaceInfo { .. } = self.device.surface_info(&surface); + + self.device + .bind_surface_to_context(&mut self.gl_context, surface) + .expect("Bind surface to context"); + + self.device + .make_context_current(&self.gl_context) + .expect("Make context current"); + + self.gl = Some(glow::Context::from_loader_function(|s| { + self.device.get_proc_address(&self.gl_context, s) + })); + + //let mut framebuffer = + // skia_safe::gpu::gl::FramebufferInfo::from_fboid(framebuffer_object); + + //framebuffer.format = gl::RGBA8; + + //framebuffer + } + + fn create_swap_chain( + &mut self, + _surface: &Self::Surface, + width: u32, + height: u32, + ) -> Self::SwapChain { + let mut surface = self + .device + .unbind_surface_from_context(&mut self.gl_context) + .expect("Unbind surface") + .expect("Active surface"); + + self.device + .resize_surface( + &self.gl_context, + &mut surface, + euclid::Size2D::new(width as i32, height as i32), + ) + .expect("Resize surface"); + + self.device + .bind_surface_to_context(&mut self.gl_context, surface) + .expect("Bind surface to context"); + + let gl = self.gl.as_ref().unwrap(); + + unsafe { + gl.viewport(0, 0, width as i32, height as i32); + 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); + } + + Viewport::new(width, height) + } + + fn draw<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + swap_chain: &mut Self::SwapChain, + output: &<Self::Renderer as iced_native::Renderer>::Output, + scale_factor: f64, + overlay: &[T], + ) -> mouse::Interaction { + let gl = self.gl.as_ref().unwrap(); + + unsafe { + gl.clear(glow::COLOR_BUFFER_BIT); + } + + let mouse = + renderer.draw(gl, swap_chain, output, scale_factor, overlay); + + { + let mut surface = self + .device + .unbind_surface_from_context(&mut self.gl_context) + .expect("Unbind surface") + .expect("Active surface"); + + self.device + .present_surface(&self.gl_context, &mut surface) + .expect("Present surface"); + + self.device + .bind_surface_to_context(&mut self.gl_context, surface) + .expect("Bind surface to context"); + } + + mouse + } +} + +impl Drop for Backend { + fn drop(&mut self) { + self.device + .destroy_context(&mut self.gl_context) + .expect("Destroy context"); + } +} diff --git a/glow/src/window/swap_chain.rs b/glow/src/window/swap_chain.rs new file mode 100644 index 00000000..41d19968 --- /dev/null +++ b/glow/src/window/swap_chain.rs @@ -0,0 +1,5 @@ +/// The rendering target of a window. +/// +/// It represents a series of virtual framebuffers with a scale factor. +#[derive(Debug)] +pub struct SwapChain; diff --git a/native/src/window/backend.rs b/native/src/window/backend.rs index 892d4bb9..d8726fd4 100644 --- a/native/src/window/backend.rs +++ b/native/src/window/backend.rs @@ -5,7 +5,7 @@ use raw_window_handle::HasRawWindowHandle; /// A graphics backend that can render to windows. pub trait Backend: Sized { /// The settings of the backend. - type Settings: Default; + type Settings: Default + Clone; /// The iced renderer of the backend. type Renderer: crate::Renderer; @@ -16,10 +16,10 @@ pub trait Backend: Sized { /// The swap chain of the backend. type SwapChain; - /// Creates a new [`Backend`] and an associated iced renderer. + /// Creates a new [`Backend`]. /// /// [`Backend`]: trait.Backend.html - fn new(settings: Self::Settings) -> (Self, Self::Renderer); + fn new(settings: Self::Settings) -> Self; /// Crates a new [`Surface`] for the given window. /// @@ -29,6 +29,11 @@ pub trait Backend: Sized { window: &W, ) -> Self::Surface; + /// Crates a new [`Renderer`]. + /// + /// [`Renderer`]: #associatedtype.Renderer + fn create_renderer(&mut self, settings: Self::Settings) -> Self::Renderer; + /// Crates a new [`SwapChain`] for the given [`Surface`]. /// /// [`SwapChain`]: #associatedtype.SwapChain diff --git a/wgpu/src/window/backend.rs b/wgpu/src/window/backend.rs index 2924ce5d..b467486c 100644 --- a/wgpu/src/window/backend.rs +++ b/wgpu/src/window/backend.rs @@ -17,8 +17,8 @@ impl iced_native::window::Backend for Backend { type Surface = wgpu::Surface; type SwapChain = SwapChain; - fn new(settings: Self::Settings) -> (Backend, Renderer) { - let (mut device, queue) = futures::executor::block_on(async { + fn new(settings: Self::Settings) -> Backend { + let (device, queue) = futures::executor::block_on(async { let adapter = wgpu::Adapter::request( &wgpu::RequestAdapterOptions { power_preference: if settings.antialiasing.is_none() { @@ -43,16 +43,15 @@ impl iced_native::window::Backend for Backend { .await }); - let renderer = Renderer::new(&mut device, settings); + Backend { + device, + queue, + format: settings.format, + } + } - ( - Backend { - device, - queue, - format: settings.format, - }, - renderer, - ) + fn create_renderer(&mut self, settings: Settings) -> Renderer { + Renderer::new(&mut self.device, settings) } fn create_surface<W: HasRawWindowHandle>( diff --git a/winit/src/application.rs b/winit/src/application.rs index f6bc8fcc..4bc36586 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -78,7 +78,11 @@ pub trait Application: Sized { /// [`update`](#tymethod.update). /// /// A `Subscription` will be kept alive as long as you keep returning it! - fn subscription(&self) -> Subscription<Self::Message>; + /// + /// By default, it returns an empty subscription. + fn subscription(&self) -> Subscription<Self::Message> { + Subscription::none() + } /// Returns the widgets to display in the [`Application`]. /// @@ -177,9 +181,10 @@ pub trait Application: Sized { let mut resized = false; let clipboard = Clipboard::new(&window); - let (mut backend, mut renderer) = Self::Backend::new(backend_settings); + let mut backend = Self::Backend::new(backend_settings.clone()); let surface = backend.create_surface(&window); + let mut renderer = backend.create_renderer(backend_settings); let mut swap_chain = { let physical_size = size.physical(); |