diff options
59 files changed, 1064 insertions, 311 deletions
diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs index bc8437a8..d142c3bc 100644 --- a/core/src/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -28,4 +28,7 @@ pub enum Event { /// A unicode character was received. CharacterReceived(char), + + /// The keyboard modifiers have changed. + ModifiersChanged(ModifiersState), } diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index cec6ac79..3186deff 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -269,7 +269,7 @@ struct ColorPicker<C: ColorSpace> { trait ColorSpace: Sized { const LABEL: &'static str; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3]; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3]; fn new(a: f32, b: f32, c: f32) -> Self; @@ -284,13 +284,25 @@ impl<C: 'static + ColorSpace + Copy> ColorPicker<C> { let [s1, s2, s3] = &mut self.sliders; let [cr1, cr2, cr3] = C::COMPONENT_RANGES; + fn slider<C>( + state: &mut slider::State, + range: RangeInclusive<f64>, + component: f32, + update: impl Fn(f32) -> C + 'static, + ) -> Slider<f64, C> { + Slider::new(state, range, f64::from(component), move |v| { + update(v as f32) + }) + .step(0.01) + } + Row::new() .spacing(10) .align_items(Align::Center) .push(Text::new(C::LABEL).width(Length::Units(50))) - .push(Slider::new(s1, cr1, c1, move |v| C::new(v, c2, c3))) - .push(Slider::new(s2, cr2, c2, move |v| C::new(c1, v, c3))) - .push(Slider::new(s3, cr3, c3, move |v| C::new(c1, c2, v))) + .push(slider(s1, cr1, c1, move |v| C::new(v, c2, c3))) + .push(slider(s2, cr2, c2, move |v| C::new(c1, v, c3))) + .push(slider(s3, cr3, c3, move |v| C::new(c1, c2, v))) .push( Text::new(color.to_string()) .width(Length::Units(185)) @@ -302,7 +314,7 @@ impl<C: 'static + ColorSpace + Copy> ColorPicker<C> { impl ColorSpace for Color { const LABEL: &'static str = "RGB"; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3] = + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = [0.0..=1.0, 0.0..=1.0, 0.0..=1.0]; fn new(r: f32, g: f32, b: f32) -> Self { @@ -325,7 +337,7 @@ impl ColorSpace for Color { impl ColorSpace for palette::Hsl { const LABEL: &'static str = "HSL"; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3] = + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; fn new(hue: f32, saturation: f32, lightness: f32) -> Self { @@ -356,7 +368,7 @@ impl ColorSpace for palette::Hsl { impl ColorSpace for palette::Hsv { const LABEL: &'static str = "HSV"; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3] = + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; fn new(hue: f32, saturation: f32, value: f32) -> Self { @@ -379,7 +391,7 @@ impl ColorSpace for palette::Hsv { impl ColorSpace for palette::Hwb { const LABEL: &'static str = "HWB"; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3] = + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; fn new(hue: f32, whiteness: f32, blackness: f32) -> Self { @@ -410,7 +422,7 @@ impl ColorSpace for palette::Hwb { impl ColorSpace for palette::Lab { const LABEL: &'static str = "Lab"; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3] = + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = [0.0..=100.0, -128.0..=127.0, -128.0..=127.0]; fn new(l: f32, a: f32, b: f32) -> Self { @@ -428,7 +440,7 @@ impl ColorSpace for palette::Lab { impl ColorSpace for palette::Lch { const LABEL: &'static str = "Lch"; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3] = + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = [0.0..=100.0, 0.0..=128.0, 0.0..=360.0]; fn new(l: f32, chroma: f32, hue: f32) -> Self { diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index e6e74995..824f9f53 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -48,24 +48,33 @@ impl Program for Controls { let sliders = Row::new() .width(Length::Units(500)) .spacing(20) - .push(Slider::new(r, 0.0..=1.0, background_color.r, move |r| { - Message::BackgroundColorChanged(Color { - r, - ..background_color + .push( + Slider::new(r, 0.0..=1.0, background_color.r, move |r| { + Message::BackgroundColorChanged(Color { + r, + ..background_color + }) }) - })) - .push(Slider::new(g, 0.0..=1.0, background_color.g, move |g| { - Message::BackgroundColorChanged(Color { - g, - ..background_color + .step(0.01), + ) + .push( + Slider::new(g, 0.0..=1.0, background_color.g, move |g| { + Message::BackgroundColorChanged(Color { + g, + ..background_color + }) }) - })) - .push(Slider::new(b, 0.0..=1.0, background_color.b, move |b| { - Message::BackgroundColorChanged(Color { - b, - ..background_color + .step(0.01), + ) + .push( + Slider::new(b, 0.0..=1.0, background_color.b, move |b| { + Message::BackgroundColorChanged(Color { + b, + ..background_color + }) }) - })); + .step(0.01), + ); Row::new() .width(Length::Fill) diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index db8b4366..33d4c361 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -5,9 +5,10 @@ use controls::Controls; use scene::Scene; use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport}; -use iced_winit::{futures, program, winit, Debug, Size}; +use iced_winit::{conversion, futures, program, winit, Debug, Size}; use winit::{ + dpi::PhysicalPosition, event::{Event, ModifiersState, WindowEvent}, event_loop::{ControlFlow, EventLoop}, }; @@ -24,6 +25,7 @@ pub fn main() { Size::new(physical_size.width, physical_size.height), window.scale_factor(), ); + let mut cursor_position = PhysicalPosition::new(-1.0, -1.0); let mut modifiers = ModifiersState::default(); // Initialize wgpu @@ -79,6 +81,7 @@ pub fn main() { let mut state = program::State::new( controls, viewport.logical_size(), + conversion::cursor_position(cursor_position, viewport.scale_factor()), &mut renderer, &mut debug, ); @@ -91,6 +94,9 @@ pub fn main() { match event { Event::WindowEvent { event, .. } => { match event { + WindowEvent::CursorMoved { position, .. } => { + cursor_position = position; + } WindowEvent::ModifiersChanged(new_modifiers) => { modifiers = new_modifiers; } @@ -105,7 +111,6 @@ pub fn main() { WindowEvent::CloseRequested => { *control_flow = ControlFlow::Exit; } - _ => {} } @@ -119,16 +124,23 @@ pub fn main() { } } Event::MainEventsCleared => { - // We update iced - let _ = state.update( - None, - viewport.logical_size(), - &mut renderer, - &mut debug, - ); + // If there are events pending + if !state.is_queue_empty() { + // We update iced + let _ = state.update( + viewport.logical_size(), + conversion::cursor_position( + cursor_position, + viewport.scale_factor(), + ), + None, + &mut renderer, + &mut debug, + ); - // and request a redraw - window.request_redraw(); + // and request a redraw + window.request_redraw(); + } } Event::RedrawRequested(_) => { if resized { diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs index 43b09928..51b56eda 100644 --- a/examples/progress_bar/src/main.rs +++ b/examples/progress_bar/src/main.rs @@ -36,12 +36,15 @@ impl Sandbox for Progress { Column::new() .padding(20) .push(ProgressBar::new(0.0..=100.0, self.value)) - .push(Slider::new( - &mut self.progress_bar_slider, - 0.0..=100.0, - self.value, - Message::SliderChanged, - )) + .push( + Slider::new( + &mut self.progress_bar_slider, + 0.0..=100.0, + self.value, + Message::SliderChanged, + ) + .step(0.01), + ) .into() } } diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index c9678b9d..4f8a4b32 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -190,7 +190,7 @@ enum Step { Welcome, Slider { state: slider::State, - value: u16, + value: u8, }, RowsAndColumns { layout: Layout, @@ -222,13 +222,13 @@ enum Step { #[derive(Debug, Clone)] pub enum StepMessage { - SliderChanged(f32), + SliderChanged(u8), LayoutChanged(Layout), - SpacingChanged(f32), - TextSizeChanged(f32), + SpacingChanged(u16), + TextSizeChanged(u16), TextColorChanged(Color), LanguageSelected(Language), - ImageWidthChanged(f32), + ImageWidthChanged(u16), InputChanged(String), ToggleSecureInput(bool), DebugToggled(bool), @@ -249,12 +249,12 @@ impl<'a> Step { } StepMessage::SliderChanged(new_value) => { if let Step::Slider { value, .. } = self { - *value = new_value.round() as u16; + *value = new_value; } } StepMessage::TextSizeChanged(new_size) => { if let Step::Text { size, .. } = self { - *size = new_size.round() as u16; + *size = new_size; } } StepMessage::TextColorChanged(new_color) => { @@ -269,12 +269,12 @@ impl<'a> Step { } StepMessage::SpacingChanged(new_spacing) => { if let Step::RowsAndColumns { spacing, .. } = self { - *spacing = new_spacing.round() as u16; + *spacing = new_spacing; } } StepMessage::ImageWidthChanged(new_width) => { if let Step::Image { width, .. } = self { - *width = new_width.round() as u16; + *width = new_width; } } StepMessage::InputChanged(new_value) => { @@ -384,7 +384,7 @@ impl<'a> Step { fn slider( state: &'a mut slider::State, - value: u16, + value: u8, ) -> Column<'a, StepMessage> { Self::container("Slider") .push(Text::new( @@ -397,8 +397,8 @@ impl<'a> Step { )) .push(Slider::new( state, - 0.0..=100.0, - value as f32, + 0..=100, + value, StepMessage::SliderChanged, )) .push( @@ -444,8 +444,8 @@ impl<'a> Step { .spacing(10) .push(Slider::new( spacing_slider, - 0.0..=80.0, - spacing as f32, + 0..=80, + spacing, StepMessage::SpacingChanged, )) .push( @@ -486,30 +486,25 @@ impl<'a> Step { ) .push(Slider::new( size_slider, - 10.0..=70.0, - size as f32, + 10..=70, + size, StepMessage::TextSizeChanged, )); let [red, green, blue] = color_sliders; + + let color_sliders = Row::new() + .spacing(10) + .push(color_slider(red, color.r, move |r| Color { r, ..color })) + .push(color_slider(green, color.g, move |g| Color { g, ..color })) + .push(color_slider(blue, color.b, move |b| Color { b, ..color })); + let color_section = Column::new() .padding(20) .spacing(20) .push(Text::new("And its color:")) .push(Text::new(&format!("{:?}", color)).color(color)) - .push( - Row::new() - .spacing(10) - .push(Slider::new(red, 0.0..=1.0, color.r, move |r| { - StepMessage::TextColorChanged(Color { r, ..color }) - })) - .push(Slider::new(green, 0.0..=1.0, color.g, move |g| { - StepMessage::TextColorChanged(Color { g, ..color }) - })) - .push(Slider::new(blue, 0.0..=1.0, color.b, move |b| { - StepMessage::TextColorChanged(Color { b, ..color }) - })), - ); + .push(color_sliders); Self::container("Text") .push(Text::new( @@ -559,8 +554,8 @@ impl<'a> Step { .push(ferris(width)) .push(Slider::new( slider, - 100.0..=500.0, - width as f32, + 100..=500, + width, StepMessage::ImageWidthChanged, )) .push( @@ -706,6 +701,17 @@ fn button<'a, Message>( .min_width(100) } +fn color_slider( + state: &mut slider::State, + component: f32, + update: impl Fn(f32) -> Color + 'static, +) -> Slider<f64, StepMessage> { + Slider::new(state, 0.0..=1.0, f64::from(component), move |c| { + StepMessage::TextColorChanged(update(c as f32)) + }) + .step(0.01) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Language { Rust, diff --git a/glow/src/backend.rs b/glow/src/backend.rs index 882dd605..8b5b4f9c 100644 --- a/glow/src/backend.rs +++ b/glow/src/backend.rs @@ -18,6 +18,7 @@ pub struct Backend { quad_pipeline: quad::Pipeline, text_pipeline: text::Pipeline, triangle_pipeline: triangle::Pipeline, + default_text_size: u16, } impl Backend { @@ -33,6 +34,7 @@ impl Backend { quad_pipeline, text_pipeline, triangle_pipeline, + default_text_size: settings.default_text_size, } } @@ -192,6 +194,10 @@ impl backend::Text for Backend { const ICON_FONT: Font = font::ICONS; const CHECKMARK_ICON: char = font::CHECKMARK_ICON; + fn default_size(&self) -> u16 { + self.default_text_size + } + fn measure( &self, contents: &str, diff --git a/glow/src/settings.rs b/glow/src/settings.rs index dce30029..c2c605ef 100644 --- a/glow/src/settings.rs +++ b/glow/src/settings.rs @@ -11,6 +11,11 @@ pub struct Settings { /// If `None` is provided, a default system font will be chosen. pub default_font: Option<&'static [u8]>, + /// The default size of text. + /// + /// By default, it will be set to 20. + pub default_text_size: u16, + /// The antialiasing strategy that will be used for triangle primitives. pub antialiasing: Option<Antialiasing>, } @@ -19,6 +24,7 @@ impl Default for Settings { fn default() -> Settings { Settings { default_font: None, + default_text_size: 20, antialiasing: None, } } diff --git a/glow/src/widget/slider.rs b/glow/src/widget/slider.rs index cf036829..3a8c2595 100644 --- a/glow/src/widget/slider.rs +++ b/glow/src/widget/slider.rs @@ -13,4 +13,4 @@ pub use iced_native::slider::State; /// 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>; +pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Renderer>; diff --git a/glow/src/window/compositor.rs b/glow/src/window/compositor.rs index 2f504ff7..3ad10b59 100644 --- a/glow/src/window/compositor.rs +++ b/glow/src/window/compositor.rs @@ -1,4 +1,4 @@ -use crate::{Backend, Renderer, Settings, Viewport}; +use crate::{Backend, Color, Renderer, Settings, Viewport}; use core::ffi::c_void; use glow::HasContext; @@ -21,8 +21,6 @@ impl iced_graphics::window::GLCompositor for Compositor { ) -> (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); @@ -60,12 +58,16 @@ impl iced_graphics::window::GLCompositor for Compositor { &mut self, renderer: &mut Self::Renderer, viewport: &Viewport, + color: Color, output: &<Self::Renderer as iced_native::Renderer>::Output, overlay: &[T], ) -> mouse::Interaction { let gl = &self.gl; + let [r, g, b, a] = color.into_linear(); + unsafe { + gl.clear_color(r, g, b, a); gl.clear(glow::COLOR_BUFFER_BIT); } diff --git a/glutin/src/application.rs b/glutin/src/application.rs index c777a13b..3be9b65f 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -47,6 +47,8 @@ pub fn run<A, E, C>( let mut title = application.title(); let mut mode = application.mode(); + let mut background_color = application.background_color(); + let mut scale_factor = application.scale_factor(); let context = { let builder = settings.window.into_builder( @@ -68,13 +70,14 @@ pub fn run<A, E, C>( }; let clipboard = Clipboard::new(&context.window()); + let mut cursor_position = glutin::dpi::PhysicalPosition::new(-1.0, -1.0); let mut mouse_interaction = mouse::Interaction::default(); let mut modifiers = glutin::event::ModifiersState::default(); let physical_size = context.window().inner_size(); let mut viewport = Viewport::with_physical_size( Size::new(physical_size.width, physical_size.height), - context.window().scale_factor(), + context.window().scale_factor() * scale_factor, ); let mut resized = false; @@ -88,6 +91,7 @@ pub fn run<A, E, C>( let mut state = program::State::new( application, viewport.logical_size(), + conversion::cursor_position(cursor_position, viewport.scale_factor()), &mut renderer, &mut debug, ); @@ -95,10 +99,18 @@ pub fn run<A, E, C>( event_loop.run(move |event, _, control_flow| match event { event::Event::MainEventsCleared => { + if state.is_queue_empty() { + return; + } + let command = runtime.enter(|| { state.update( - clipboard.as_ref().map(|c| c as _), viewport.logical_size(), + conversion::cursor_position( + cursor_position, + viewport.scale_factor(), + ), + clipboard.as_ref().map(|c| c as _), &mut renderer, &mut debug, ) @@ -134,6 +146,39 @@ pub fn run<A, E, C>( mode = new_mode; } + + // Update background color + background_color = program.background_color(); + + // Update scale factor + let new_scale_factor = program.scale_factor(); + + if scale_factor != new_scale_factor { + let size = context.window().inner_size(); + + viewport = Viewport::with_physical_size( + Size::new(size.width, size.height), + context.window().scale_factor() * new_scale_factor, + ); + + // We relayout the UI with the new logical size. + // The queue is empty, therefore this will never produce + // a `Command`. + // + // TODO: Properly queue `WindowResized` + let _ = state.update( + viewport.logical_size(), + conversion::cursor_position( + cursor_position, + viewport.scale_factor(), + ), + clipboard.as_ref().map(|c| c as _), + &mut renderer, + &mut debug, + ); + + scale_factor = new_scale_factor; + } } context.window().request_redraw(); @@ -160,6 +205,7 @@ pub fn run<A, E, C>( let new_mouse_interaction = compositor.draw( &mut renderer, &viewport, + background_color, state.primitive(), &debug.overlay(), ); @@ -186,7 +232,9 @@ pub fn run<A, E, C>( application::handle_window_event( &window_event, context.window(), + scale_factor, control_flow, + &mut cursor_position, &mut modifiers, &mut viewport, &mut resized, diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 83510311..b73c636e 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -25,6 +25,9 @@ pub trait Text { /// [`ICON_FONT`]: #associatedconst.ICON_FONt const CHECKMARK_ICON: char; + /// Returns the default size of text. + fn default_size(&self) -> u16; + /// Measures the text contents with the given size and font, /// returning the size of a laid out paragraph that fits in the provided /// bounds. diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index b6dda132..38d8dffa 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -9,7 +9,6 @@ #![forbid(rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg))] mod antialiasing; -mod defaults; mod primitive; mod renderer; mod transformation; @@ -17,6 +16,7 @@ mod viewport; mod widget; pub mod backend; +pub mod defaults; pub mod font; pub mod layer; pub mod triangle; @@ -35,6 +35,6 @@ pub use transformation::Transformation; pub use viewport::Viewport; pub use iced_native::{ - Background, Font, HorizontalAlignment, Point, Rectangle, Size, Vector, - VerticalAlignment, + Background, Color, Font, HorizontalAlignment, Point, Rectangle, Size, + Vector, VerticalAlignment, }; diff --git a/graphics/src/widget/button.rs b/graphics/src/widget/button.rs index aeb862d5..ecabc868 100644 --- a/graphics/src/widget/button.rs +++ b/graphics/src/widget/button.rs @@ -103,7 +103,7 @@ where } else { content }, - if is_mouse_over { + if is_mouse_over && !is_disabled { mouse::Interaction::Pointer } else { mouse::Interaction::default() diff --git a/graphics/src/widget/canvas/path/builder.rs b/graphics/src/widget/canvas/path/builder.rs index 6511fa52..e0e52845 100644 --- a/graphics/src/widget/canvas/path/builder.rs +++ b/graphics/src/widget/canvas/path/builder.rs @@ -84,7 +84,7 @@ impl Builder { radii: math::Vector::new(arc.radii.x, arc.radii.y), x_rotation: math::Angle::radians(arc.rotation), start_angle: math::Angle::radians(arc.start_angle), - sweep_angle: math::Angle::radians(arc.end_angle), + sweep_angle: math::Angle::radians(arc.end_angle - arc.start_angle), }; let _ = self.raw.move_to(arc.sample(0.0)); diff --git a/graphics/src/widget/slider.rs b/graphics/src/widget/slider.rs index b00cde9a..da8b5a86 100644 --- a/graphics/src/widget/slider.rs +++ b/graphics/src/widget/slider.rs @@ -16,8 +16,8 @@ pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; /// values. /// /// This is an alias of an `iced_native` slider with an `iced_wgpu::Renderer`. -pub type Slider<'a, Message, Backend> = - iced_native::Slider<'a, Message, Renderer<Backend>>; +pub type Slider<'a, T, Message, Backend> = + iced_native::Slider<'a, T, Message, Renderer<Backend>>; const HANDLE_HEIGHT: f32 = 22.0; diff --git a/graphics/src/widget/text.rs b/graphics/src/widget/text.rs index 327f8e29..7e22e680 100644 --- a/graphics/src/widget/text.rs +++ b/graphics/src/widget/text.rs @@ -20,7 +20,9 @@ where { type Font = Font; - const DEFAULT_SIZE: u16 = 20; + fn default_size(&self) -> u16 { + self.backend().default_size() + } fn measure( &self, diff --git a/graphics/src/widget/text_input.rs b/graphics/src/widget/text_input.rs index f13f6606..575d67f5 100644 --- a/graphics/src/widget/text_input.rs +++ b/graphics/src/widget/text_input.rs @@ -27,14 +27,8 @@ impl<B> text_input::Renderer for Renderer<B> where B: Backend + backend::Text, { - type Font = Font; 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 backend = self.backend(); diff --git a/graphics/src/window/compositor.rs b/graphics/src/window/compositor.rs index d5920c95..aa625f43 100644 --- a/graphics/src/window/compositor.rs +++ b/graphics/src/window/compositor.rs @@ -1,4 +1,4 @@ -use crate::Viewport; +use crate::{Color, Viewport}; use iced_native::mouse; use raw_window_handle::HasRawWindowHandle; @@ -49,6 +49,7 @@ pub trait Compositor: Sized { renderer: &mut Self::Renderer, swap_chain: &mut Self::SwapChain, viewport: &Viewport, + background_color: Color, output: &<Self::Renderer as iced_native::Renderer>::Output, overlay: &[T], ) -> mouse::Interaction; diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs index 542213b5..2ba39d6e 100644 --- a/graphics/src/window/gl_compositor.rs +++ b/graphics/src/window/gl_compositor.rs @@ -1,4 +1,4 @@ -use crate::{Size, Viewport}; +use crate::{Color, Size, Viewport}; use iced_native::mouse; use core::ffi::c_void; @@ -61,6 +61,7 @@ pub trait GLCompositor: Sized { &mut self, renderer: &mut Self::Renderer, viewport: &Viewport, + background_color: Color, output: &<Self::Renderer as iced_native::Renderer>::Output, overlay: &[T], ) -> mouse::Interaction; diff --git a/native/Cargo.toml b/native/Cargo.toml index 75b4a56b..13052a93 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -13,6 +13,7 @@ debug = [] [dependencies] twox-hash = "1.5" unicode-segmentation = "1.6" +num-traits = "0.2" [dependencies.iced_core] version = "0.2" diff --git a/native/src/debug/basic.rs b/native/src/debug/basic.rs index 5338d0d9..8a712038 100644 --- a/native/src/debug/basic.rs +++ b/native/src/debug/basic.rs @@ -174,9 +174,13 @@ impl Debug { lines.push(key_value("Render:", self.render_durations.average())); lines.push(key_value("Message count:", self.message_count)); lines.push(String::from("Last messages:")); - lines.extend( - self.last_messages.iter().map(|msg| format!(" {}", msg)), - ); + lines.extend(self.last_messages.iter().map(|msg| { + if msg.len() <= 100 { + format!(" {}", msg) + } else { + format!(" {:.100}...", msg) + } + })); lines } diff --git a/native/src/program/state.rs b/native/src/program/state.rs index 8716d8b9..fdc42e8b 100644 --- a/native/src/program/state.rs +++ b/native/src/program/state.rs @@ -1,5 +1,5 @@ use crate::{ - Cache, Clipboard, Command, Debug, Event, Program, Renderer, Size, + Cache, Clipboard, Command, Debug, Event, Point, Program, Renderer, Size, UserInterface, }; @@ -31,6 +31,7 @@ where pub fn new( mut program: P, bounds: Size, + cursor_position: Point, renderer: &mut P::Renderer, debug: &mut Debug, ) -> Self { @@ -43,7 +44,7 @@ where ); debug.draw_started(); - let primitive = user_interface.draw(renderer); + let primitive = user_interface.draw(renderer, cursor_position); debug.draw_finished(); let cache = Some(user_interface.into_cache()); @@ -88,6 +89,13 @@ where self.queued_messages.push(message); } + /// Returns whether the event queue of the [`State`] is empty or not. + /// + /// [`State`]: struct.State.html + pub fn is_queue_empty(&self) -> bool { + self.queued_events.is_empty() && self.queued_messages.is_empty() + } + /// Processes all the queued events and messages, rebuilding and redrawing /// the widgets of the linked [`Program`] if necessary. /// @@ -97,15 +105,12 @@ where /// [`Program`]: trait.Program.html pub fn update( &mut self, - clipboard: Option<&dyn Clipboard>, bounds: Size, + cursor_position: Point, + clipboard: Option<&dyn Clipboard>, renderer: &mut P::Renderer, debug: &mut Debug, ) -> Option<Command<P::Message>> { - if self.queued_events.is_empty() && self.queued_messages.is_empty() { - return None; - } - let mut user_interface = build_user_interface( &mut self.program, self.cache.take().unwrap(), @@ -117,6 +122,7 @@ where debug.event_processing_started(); let mut messages = user_interface.update( self.queued_events.drain(..), + cursor_position, clipboard, renderer, ); @@ -125,7 +131,7 @@ where if messages.is_empty() { debug.draw_started(); - self.primitive = user_interface.draw(renderer); + self.primitive = user_interface.draw(renderer, cursor_position); debug.draw_finished(); self.cache = Some(user_interface.into_cache()); @@ -156,7 +162,7 @@ where ); debug.draw_started(); - self.primitive = user_interface.draw(renderer); + self.primitive = user_interface.draw(renderer, cursor_position); debug.draw_finished(); self.cache = Some(user_interface.into_cache()); diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index d6a8e18f..580f58f8 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -50,7 +50,9 @@ impl row::Renderer for Null { impl text::Renderer for Null { type Font = Font; - const DEFAULT_SIZE: u16 = 20; + fn default_size(&self) -> u16 { + 20 + } fn measure( &self, @@ -104,13 +106,8 @@ impl scrollable::Renderer for Null { } impl text_input::Renderer for Null { - type Font = Font; type Style = (); - fn default_size(&self) -> u16 { - 20 - } - fn measure_value(&self, _value: &str, _size: u16, _font: Font) -> f32 { 0.0 } diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index e963b601..b9646043 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,4 +1,4 @@ -use crate::{layout, mouse, Clipboard, Element, Event, Layout, Point, Size}; +use crate::{layout, Clipboard, Element, Event, Layout, Point, Size}; use std::hash::Hasher; @@ -23,7 +23,6 @@ pub struct UserInterface<'a, Message, Renderer> { root: Element<'a, Message, Renderer>, layout: layout::Node, bounds: Size, - cursor_position: Point, } impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> @@ -115,7 +114,6 @@ where root, layout, bounds, - cursor_position: cache.cursor_position, } } @@ -132,7 +130,7 @@ where /// completing [the previous example](#example): /// /// ```no_run - /// use iced_native::{UserInterface, Cache, Size}; + /// use iced_native::{UserInterface, Cache, Size, Point}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { @@ -154,6 +152,7 @@ where /// let mut cache = Cache::new(); /// let mut renderer = Renderer::new(); /// let mut window_size = Size::new(1024.0, 768.0); + /// let mut cursor_position = Point::default(); /// /// // Initialize our event storage /// let mut events = Vec::new(); @@ -169,7 +168,12 @@ where /// ); /// /// // Update the user interface - /// let messages = user_interface.update(events.drain(..), None, &renderer); + /// let messages = user_interface.update( + /// events.drain(..), + /// cursor_position, + /// None, + /// &renderer, + /// ); /// /// cache = user_interface.into_cache(); /// @@ -182,20 +186,17 @@ where pub fn update( &mut self, events: impl IntoIterator<Item = Event>, + cursor_position: Point, clipboard: Option<&dyn Clipboard>, renderer: &Renderer, ) -> Vec<Message> { let mut messages = Vec::new(); for event in events { - if let Event::Mouse(mouse::Event::CursorMoved { x, y }) = event { - self.cursor_position = Point::new(x, y); - } - self.root.widget.on_event( event, Layout::new(&self.layout), - self.cursor_position, + cursor_position, &mut messages, renderer, clipboard, @@ -219,7 +220,7 @@ where /// [completing the last example](#example-1): /// /// ```no_run - /// use iced_native::{UserInterface, Cache, Size}; + /// use iced_native::{UserInterface, Cache, Size, Point}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { @@ -241,6 +242,7 @@ where /// let mut cache = Cache::new(); /// let mut renderer = Renderer::new(); /// let mut window_size = Size::new(1024.0, 768.0); + /// let mut cursor_position = Point::default(); /// let mut events = Vec::new(); /// /// loop { @@ -253,10 +255,15 @@ where /// &mut renderer, /// ); /// - /// let messages = user_interface.update(events.drain(..), None, &renderer); + /// let messages = user_interface.update( + /// events.drain(..), + /// cursor_position, + /// None, + /// &renderer, + /// ); /// /// // Draw the user interface - /// let mouse_cursor = user_interface.draw(&mut renderer); + /// let mouse_cursor = user_interface.draw(&mut renderer, cursor_position); /// /// cache = user_interface.into_cache(); /// @@ -268,12 +275,16 @@ where /// // Flush rendering operations... /// } /// ``` - pub fn draw(&self, renderer: &mut Renderer) -> Renderer::Output { + pub fn draw( + &self, + renderer: &mut Renderer, + cursor_position: Point, + ) -> Renderer::Output { self.root.widget.draw( renderer, &Renderer::Defaults::default(), Layout::new(&self.layout), - self.cursor_position, + cursor_position, ) } @@ -287,7 +298,6 @@ where hash: self.hash, layout: self.layout, bounds: self.bounds, - cursor_position: self.cursor_position, } } } @@ -300,7 +310,6 @@ pub struct Cache { hash: u64, layout: layout::Node, bounds: Size, - cursor_position: Point, } impl Cache { @@ -316,7 +325,6 @@ impl Cache { hash: 0, layout: layout::Node::new(Size::new(0.0, 0.0)), bounds: Size::ZERO, - cursor_position: Point::new(-1.0, -1.0), } } } @@ -329,7 +337,7 @@ impl Default for Cache { impl PartialEq for Cache { fn eq(&self, other: &Cache) -> bool { - self.hash == other.hash && self.cursor_position == other.cursor_position + self.hash == other.hash } } diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 5fb13290..44962288 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -32,7 +32,7 @@ pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> { width: Length, size: u16, spacing: u16, - text_size: u16, + text_size: Option<u16>, style: Renderer::Style, } @@ -60,7 +60,7 @@ impl<Message, Renderer: self::Renderer + text::Renderer> width: Length::Shrink, size: <Renderer as self::Renderer>::DEFAULT_SIZE, spacing: Renderer::DEFAULT_SPACING, - text_size: <Renderer as text::Renderer>::DEFAULT_SIZE, + text_size: None, style: Renderer::Style::default(), } } @@ -93,7 +93,7 @@ impl<Message, Renderer: self::Renderer + text::Renderer> /// /// [`Checkbox`]: struct.Checkbox.html pub fn text_size(mut self, text_size: u16) -> Self { - self.text_size = text_size; + self.text_size = Some(text_size); self } @@ -136,7 +136,7 @@ where .push( Text::new(&self.label) .width(self.width) - .size(self.text_size), + .size(self.text_size.unwrap_or(renderer.default_size())), ) .layout(renderer, limits) } @@ -181,7 +181,7 @@ where defaults, label_layout.bounds(), &self.label, - self.text_size, + self.text_size.unwrap_or(renderer.default_size()), Default::default(), None, HorizontalAlignment::Left, diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index a3282ed7..21788e6a 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -216,6 +216,10 @@ where /// The `leeway` describes the amount of space around a split that can be /// used to grab it. /// + /// The grabbable area of a split will have a length of `spacing + leeway`, + /// properly centered. In other words, a length of + /// `(spacing + leeway) / 2.0` on either side of the split line. + /// /// [`PaneGrid`]: struct.PaneGrid.html pub fn on_resize<F>(mut self, leeway: u16, f: F) -> Self where @@ -284,8 +288,6 @@ where self.state.focus(pane); } } - } else { - self.state.unfocus(); } } @@ -299,7 +301,7 @@ where if let Some((split, _)) = self.state.picked_split() { let bounds = layout.bounds(); - let splits = self.state.splits( + let splits = self.state.split_regions( f32::from(self.spacing), Size::new(bounds.width, bounds.height), ); @@ -421,7 +423,7 @@ where let limits = limits.width(self.width).height(self.height); let size = limits.resolve(Size::ZERO); - let regions = self.state.regions(f32::from(self.spacing), size); + let regions = self.state.pane_regions(f32::from(self.spacing), size); let children = self .elements @@ -464,7 +466,7 @@ where cursor_position.y - bounds.y, ); - let splits = self.state.splits( + let splits = self.state.split_regions( f32::from(self.spacing), Size::new(bounds.width, bounds.height), ); @@ -493,6 +495,8 @@ where ); } } + } else { + self.state.unfocus(); } } mouse::Event::ButtonReleased(mouse::Button::Left) => { @@ -550,10 +554,8 @@ where } } } - - *self.pressed_modifiers = modifiers; } - keyboard::Event::KeyReleased { modifiers, .. } => { + keyboard::Event::ModifiersChanged(modifiers) => { *self.pressed_modifiers = modifiers; } _ => {} @@ -601,7 +603,7 @@ where let splits = self .state - .splits(f32::from(self.spacing), bounds.size()); + .split_regions(f32::from(self.spacing), bounds.size()); hovered_split( splits.iter(), diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs index b13c5e26..cbfd8a43 100644 --- a/native/src/widget/pane_grid/node.rs +++ b/native/src/widget/pane_grid/node.rs @@ -43,12 +43,36 @@ pub enum Node { } impl Node { + /// Returns an iterator over each [`Split`] in this [`Node`]. + /// + /// [`Split`]: struct.Split.html + /// [`Node`]: enum.Node.html + pub fn splits(&self) -> impl Iterator<Item = &Split> { + let mut unvisited_nodes = vec![self]; + + std::iter::from_fn(move || { + while let Some(node) = unvisited_nodes.pop() { + match node { + Node::Split { id, a, b, .. } => { + unvisited_nodes.push(a); + unvisited_nodes.push(b); + + return Some(id); + } + _ => {} + } + } + + None + }) + } + /// Returns the rectangular region for each [`Pane`] in the [`Node`] given /// the spacing between panes and the total available space. /// /// [`Pane`]: struct.Pane.html /// [`Node`]: enum.Node.html - pub fn regions( + pub fn pane_regions( &self, spacing: f32, size: Size, @@ -75,7 +99,7 @@ impl Node { /// /// [`Split`]: struct.Split.html /// [`Node`]: enum.Node.html - pub fn splits( + pub fn split_regions( &self, spacing: f32, size: Size, diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index a8431dec..3f0accc1 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -157,8 +157,10 @@ impl<T> State<T> { /// [`Pane`]: struct.Pane.html /// [`State::active`]: struct.State.html#method.active pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> { - let regions = - self.internal.layout.regions(0.0, Size::new(4096.0, 4096.0)); + let regions = self + .internal + .layout + .pane_regions(0.0, Size::new(4096.0, 4096.0)); let current_region = regions.get(pane)?; @@ -194,6 +196,13 @@ impl<T> State<T> { self.internal.focus(pane); } + /// Unfocuses the current focused [`Pane`]. + /// + /// [`Pane`]: struct.Pane.html + pub fn unfocus(&mut self) { + self.internal.unfocus(); + } + /// Splits the given [`Pane`] into two in the given [`Axis`] and /// initializing the new [`Pane`] with the provided internal state. /// @@ -366,20 +375,20 @@ impl Internal { } } - pub fn regions( + pub fn pane_regions( &self, spacing: f32, size: Size, ) -> HashMap<Pane, Rectangle> { - self.layout.regions(spacing, size) + self.layout.pane_regions(spacing, size) } - pub fn splits( + pub fn split_regions( &self, spacing: f32, size: Size, ) -> HashMap<Split, (Axis, Rectangle, f32)> { - self.layout.splits(spacing, size) + self.layout.split_regions(spacing, size) } pub fn focus(&mut self, pane: &Pane) { diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index d07a9012..5b8d00e9 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -41,7 +41,7 @@ pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> { width: Length, size: u16, spacing: u16, - text_size: u16, + text_size: Option<u16>, style: Renderer::Style, } @@ -75,7 +75,7 @@ impl<Message, Renderer: self::Renderer + text::Renderer> width: Length::Shrink, size: <Renderer as self::Renderer>::DEFAULT_SIZE, spacing: Renderer::DEFAULT_SPACING, //15 - text_size: <Renderer as text::Renderer>::DEFAULT_SIZE, + text_size: None, style: Renderer::Style::default(), } } @@ -108,7 +108,7 @@ impl<Message, Renderer: self::Renderer + text::Renderer> /// /// [`Radio`]: struct.Radio.html pub fn text_size(mut self, text_size: u16) -> Self { - self.text_size = text_size; + self.text_size = Some(text_size); self } @@ -151,7 +151,7 @@ where .push( Text::new(&self.label) .width(self.width) - .size(self.text_size), + .size(self.text_size.unwrap_or(renderer.default_size())), ) .layout(renderer, limits) } @@ -194,7 +194,7 @@ where defaults, label_layout.bounds(), &self.label, - self.text_size, + self.text_size.unwrap_or(renderer.default_size()), Default::default(), None, HorizontalAlignment::Left, diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 753a49fe..70f2b6ac 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -16,13 +16,16 @@ use std::{hash::Hash, ops::RangeInclusive}; /// /// A [`Slider`] will try to fill the horizontal space of its container. /// +/// The [`Slider`] range of numeric values is generic and its step size defaults +/// to 1 unit. +/// /// [`Slider`]: struct.Slider.html /// /// # Example /// ``` /// # use iced_native::{slider, renderer::Null}; /// # -/// # pub type Slider<'a, Message> = iced_native::Slider<'a, Message, Null>; +/// # pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Null>; /// pub enum Message { /// SliderChanged(f32), /// } @@ -35,17 +38,22 @@ use std::{hash::Hash, ops::RangeInclusive}; /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, Message, Renderer: self::Renderer> { +pub struct Slider<'a, T, Message, Renderer: self::Renderer> { state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, - on_change: Box<dyn Fn(f32) -> Message>, + range: RangeInclusive<T>, + step: T, + value: T, + on_change: Box<dyn Fn(T) -> Message>, on_release: Option<Message>, width: Length, style: Renderer::Style, } -impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> { +impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Renderer: self::Renderer, +{ /// Creates a new [`Slider`]. /// /// It expects: @@ -60,17 +68,30 @@ impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> { /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, + range: RangeInclusive<T>, + value: T, on_change: F, ) -> Self where - F: 'static + Fn(f32) -> Message, + F: 'static + Fn(T) -> Message, { + let value = if value >= *range.start() { + value + } else { + *range.start() + }; + + let value = if value <= *range.end() { + value + } else { + *range.end() + }; + Slider { state, - value: value.max(*range.start()).min(*range.end()), + value, range, + step: T::from(1), on_change: Box::new(on_change), on_release: None, width: Length::Fill, @@ -106,6 +127,14 @@ impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> { self.style = style.into(); self } + + /// Sets the step size of the [`Slider`]. + /// + /// [`Slider`]: struct.Slider.html + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } } /// The local state of a [`Slider`]. @@ -125,9 +154,10 @@ impl State { } } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for Slider<'a, Message, Renderer> +impl<'a, T, Message, Renderer> Widget<Message, Renderer> + for Slider<'a, T, Message, Renderer> where + T: Copy + Into<f64> + num_traits::FromPrimitive, Renderer: self::Renderer, Message: Clone, { @@ -164,17 +194,24 @@ where ) { let mut change = || { let bounds = layout.bounds(); - if cursor_position.x <= bounds.x { messages.push((self.on_change)(*self.range.start())); } else if cursor_position.x >= bounds.x + bounds.width { messages.push((self.on_change)(*self.range.end())); } else { - let percent = (cursor_position.x - bounds.x) / bounds.width; - let value = (self.range.end() - self.range.start()) * percent - + self.range.start(); + let step = self.step.into(); + let start = (*self.range.start()).into(); + let end = (*self.range.end()).into(); + + let percent = f64::from(cursor_position.x - bounds.x) + / f64::from(bounds.width); + + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; - messages.push((self.on_change)(value)); + if let Some(value) = T::from_f64(value) { + messages.push((self.on_change)(value)); + } } }; @@ -212,11 +249,14 @@ where layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { + let start = *self.range.start(); + let end = *self.range.end(); + renderer.draw( layout.bounds(), cursor_position, - self.range.clone(), - self.value, + start.into() as f32..=end.into() as f32, + self.value.into() as f32, self.state.is_dragging, &self.style, ) @@ -269,14 +309,15 @@ pub trait Renderer: crate::Renderer { ) -> Self::Output; } -impl<'a, Message, Renderer> From<Slider<'a, Message, Renderer>> +impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>> for Element<'a, Message, Renderer> where + T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, Renderer: 'a + self::Renderer, Message: 'a + Clone, { fn from( - slider: Slider<'a, Message, Renderer>, + slider: Slider<'a, T, Message, Renderer>, ) -> Element<'a, Message, Renderer> { Element::new(slider) } diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index 0b05b67d..48a69e34 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -131,7 +131,7 @@ where ) -> layout::Node { let limits = limits.width(self.width).height(self.height); - let size = self.size.unwrap_or(Renderer::DEFAULT_SIZE); + let size = self.size.unwrap_or(renderer.default_size()); let bounds = limits.max(); @@ -154,7 +154,7 @@ where defaults, layout.bounds(), &self.content, - self.size.unwrap_or(Renderer::DEFAULT_SIZE), + self.size.unwrap_or(renderer.default_size()), self.font, self.color, self.horizontal_alignment, @@ -187,10 +187,10 @@ pub trait Renderer: crate::Renderer { /// [`Text`]: struct.Text.html type Font: Default + Copy; - /// The default size of [`Text`]. + /// Returns the default size of [`Text`]. /// /// [`Text`]: struct.Text.html - const DEFAULT_SIZE: u16; + fn default_size(&self) -> u16; /// Measures the [`Text`] in the given bounds and returns the minimum /// boundaries that can fit the contents. diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index e3a5355b..3f415101 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -17,8 +17,8 @@ use editor::Editor; use crate::{ keyboard, layout, mouse::{self, click}, - Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Size, - Widget, + text, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, + Size, Widget, }; use std::u32; @@ -486,7 +486,8 @@ where let text_bounds = layout.children().next().unwrap().bounds(); if self.is_secure { - renderer.draw( + self::Renderer::draw( + renderer, bounds, text_bounds, cursor_position, @@ -498,7 +499,8 @@ where &self.style, ) } else { - renderer.draw( + self::Renderer::draw( + renderer, bounds, text_bounds, cursor_position, @@ -531,20 +533,10 @@ where /// /// [`TextInput`]: struct.TextInput.html /// [renderer]: ../../renderer/index.html -pub trait Renderer: crate::Renderer + Sized { - /// The font type used for [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - type Font: Default + Copy; - +pub trait Renderer: text::Renderer + Sized { /// The style supported by this renderer. type Style: Default; - /// Returns the default size of the text of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - fn default_size(&self) -> u16; - /// Returns the width of the value of the [`TextInput`]. /// /// [`TextInput`]: struct.TextInput.html @@ -683,6 +675,30 @@ impl State { pub fn cursor(&self) -> Cursor { self.cursor } + + /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. + /// + /// [`Cursor`]: struct.Cursor.html + /// [`TextInput`]: struct.TextInput.html + pub fn move_cursor_to_front(&mut self) { + self.cursor.move_to(0); + } + + /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text. + /// + /// [`Cursor`]: struct.Cursor.html + /// [`TextInput`]: struct.TextInput.html + pub fn move_cursor_to_end(&mut self) { + self.cursor.move_to(usize::MAX); + } + + /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location. + /// + /// [`Cursor`]: struct.Cursor.html + /// [`TextInput`]: struct.TextInput.html + pub fn move_cursor_to(&mut self, position: usize) { + self.cursor.move_to(position); + } } // TODO: Reduce allocations diff --git a/src/application.rs b/src/application.rs index 19cab7da..47b89dbd 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,4 +1,6 @@ -use crate::{window, Command, Element, Executor, Settings, Subscription}; +use crate::{ + window, Color, Command, Element, Executor, Settings, Subscription, +}; /// An interactive cross-platform application. /// @@ -174,6 +176,31 @@ pub trait Application: Sized { window::Mode::Windowed } + /// Returns the background color of the [`Application`]. + /// + /// By default, it returns [`Color::WHITE`]. + /// + /// [`Application`]: trait.Application.html + /// [`Color::WHITE`]: struct.Color.html#const.WHITE + fn background_color(&self) -> Color { + Color::WHITE + } + + /// Returns the scale factor of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + /// + /// [`Application`]: trait.Application.html + fn scale_factor(&self) -> f64 { + 1.0 + } + /// Runs the [`Application`]. /// /// On native platforms, this method will take control of the current thread @@ -190,6 +217,7 @@ pub trait Application: Sized { { let renderer_settings = crate::renderer::Settings { default_font: settings.default_font, + default_text_size: settings.default_text_size, antialiasing: if settings.antialiasing { Some(crate::renderer::settings::Antialiasing::MSAAx4) } else { @@ -256,6 +284,14 @@ where fn subscription(&self) -> Subscription<Self::Message> { self.0.subscription() } + + fn background_color(&self) -> Color { + self.0.background_color() + } + + fn scale_factor(&self) -> f64 { + self.0.scale_factor() + } } #[cfg(target_arch = "wasm32")] diff --git a/src/sandbox.rs b/src/sandbox.rs index c6fa45d0..6a73eab0 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -1,4 +1,6 @@ -use crate::{executor, Application, Command, Element, Settings, Subscription}; +use crate::{ + executor, Application, Color, Command, Element, Settings, Subscription, +}; /// A sandboxed [`Application`]. /// @@ -124,6 +126,31 @@ pub trait Sandbox { /// [`Sandbox`]: trait.Sandbox.html fn view(&mut self) -> Element<'_, Self::Message>; + /// Returns the background color of the [`Sandbox`]. + /// + /// By default, it returns [`Color::WHITE`]. + /// + /// [`Sandbox`]: trait.Sandbox.html + /// [`Color::WHITE`]: struct.Color.html#const.WHITE + fn background_color(&self) -> Color { + Color::WHITE + } + + /// Returns the scale factor of the [`Sandbox`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + /// + /// [`Sandbox`]: trait.Sandbox.html + fn scale_factor(&self) -> f64 { + 1.0 + } + /// Runs the [`Sandbox`]. /// /// On native platforms, this method will take control of the current thread @@ -169,4 +196,12 @@ where fn view(&mut self) -> Element<'_, T::Message> { T::view(self) } + + fn background_color(&self) -> Color { + T::background_color(self) + } + + fn scale_factor(&self) -> f64 { + T::scale_factor(self) + } } diff --git a/src/settings.rs b/src/settings.rs index 01ad0ee0..40b1b1ea 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,7 +2,7 @@ use crate::window; /// The settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone)] pub struct Settings<Flags> { /// The window settings. /// @@ -22,6 +22,11 @@ pub struct Settings<Flags> { // TODO: Add `name` for web compatibility pub default_font: Option<&'static [u8]>, + /// The text size that will be used by default. + /// + /// The default value is 20. + pub default_text_size: u16, + /// If set to true, the renderer will try to perform antialiasing for some /// primitives. /// @@ -39,12 +44,28 @@ impl<Flags> Settings<Flags> { /// /// [`Application`]: ../trait.Application.html pub fn with_flags(flags: Flags) -> Self { + let default_settings = Settings::<()>::default(); + Self { flags, - // not using ..Default::default() struct update syntax since it is more permissive to - // allow initializing with flags without trait bound on Default + antialiasing: default_settings.antialiasing, + default_font: default_settings.default_font, + default_text_size: default_settings.default_text_size, + window: default_settings.window, + } + } +} + +impl<Flags> Default for Settings<Flags> +where + Flags: Default, +{ + fn default() -> Self { + Self { + flags: Default::default(), antialiasing: Default::default(), default_font: Default::default(), + default_text_size: 20, window: Default::default(), } } @@ -54,12 +75,7 @@ impl<Flags> Settings<Flags> { impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> { fn from(settings: Settings<Flags>) -> iced_winit::Settings<Flags> { iced_winit::Settings { - window: iced_winit::settings::Window { - size: settings.window.size, - resizable: settings.window.resizable, - decorations: settings.window.decorations, - platform_specific: Default::default(), - }, + window: settings.window.into(), flags: settings.flags, } } diff --git a/src/widget.rs b/src/widget.rs index 3e4d4788..007bd531 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -50,7 +50,7 @@ mod platform { text_input::TextInput, }; - #[cfg(feature = "canvas")] + #[cfg(any(feature = "canvas", feature = "glow_canvas"))] #[doc(no_inline)] pub use canvas::Canvas; } diff --git a/src/window.rs b/src/window.rs index 54ea2a02..a2883b62 100644 --- a/src/window.rs +++ b/src/window.rs @@ -2,5 +2,8 @@ mod mode; mod settings; +pub mod icon; + +pub use icon::Icon; pub use mode::Mode; pub use settings::Settings; diff --git a/src/window/icon.rs b/src/window/icon.rs new file mode 100644 index 00000000..15e0312d --- /dev/null +++ b/src/window/icon.rs @@ -0,0 +1,132 @@ +//! Attach an icon to the window of your application. +use std::fmt; +use std::io; + +/// The icon of a window. +#[cfg(not(target_arch = "wasm32"))] +#[derive(Debug, Clone)] +pub struct Icon(iced_winit::winit::window::Icon); + +/// The icon of a window. +#[cfg(target_arch = "wasm32")] +#[derive(Debug, Clone)] +pub struct Icon; + +impl Icon { + /// Creates an icon from 32bpp RGBA data. + #[cfg(not(target_arch = "wasm32"))] + pub fn from_rgba( + rgba: Vec<u8>, + width: u32, + height: u32, + ) -> Result<Self, Error> { + let raw = + iced_winit::winit::window::Icon::from_rgba(rgba, width, height)?; + + Ok(Icon(raw)) + } + + /// Creates an icon from 32bpp RGBA data. + #[cfg(target_arch = "wasm32")] + pub fn from_rgba( + _rgba: Vec<u8>, + _width: u32, + _height: u32, + ) -> Result<Self, Error> { + Ok(Icon) + } +} + +/// An error produced when using `Icon::from_rgba` with invalid arguments. +#[derive(Debug)] +pub enum Error { + /// The provided RGBA data isn't divisble by 4. + /// + /// Therefore, it cannot be safely interpreted as 32bpp RGBA pixels. + InvalidData { + /// The length of the provided RGBA data. + byte_count: usize, + }, + + /// The number of RGBA pixels does not match the provided dimensions. + DimensionsMismatch { + /// The provided width. + width: u32, + /// The provided height. + height: u32, + /// The amount of pixels of the provided RGBA data. + pixel_count: usize, + }, + + /// The underlying OS failed to create the icon. + OsError(io::Error), +} + +#[cfg(not(target_arch = "wasm32"))] +impl From<iced_winit::winit::window::BadIcon> for Error { + fn from(error: iced_winit::winit::window::BadIcon) -> Self { + use iced_winit::winit::window::BadIcon; + + match error { + BadIcon::ByteCountNotDivisibleBy4 { byte_count } => { + Error::InvalidData { byte_count } + } + BadIcon::DimensionsVsPixelCount { + width, + height, + pixel_count, + .. + } => Error::DimensionsMismatch { + width, + height, + pixel_count, + }, + BadIcon::OsError(os_error) => Error::OsError(os_error), + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From<Icon> for iced_winit::winit::window::Icon { + fn from(icon: Icon) -> Self { + icon.0 + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::InvalidData { byte_count } => { + write!(f, + "The provided RGBA data (with length {:?}) isn't divisble by \ + 4. Therefore, it cannot be safely interpreted as 32bpp RGBA \ + pixels.", + byte_count, + ) + } + Error::DimensionsMismatch { + width, + height, + pixel_count, + } => { + write!(f, + "The number of RGBA pixels ({:?}) does not match the provided \ + dimensions ({:?}x{:?}).", + width, height, pixel_count, + ) + } + Error::OsError(e) => write!( + f, + "The underlying OS failed to create the window \ + icon: {:?}", + e + ), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(self) + } +} diff --git a/src/window/settings.rs b/src/window/settings.rs index a31d2af2..2046f2d9 100644 --- a/src/window/settings.rs +++ b/src/window/settings.rs @@ -1,22 +1,51 @@ +use crate::window::Icon; + /// The window settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct Settings { - /// The size of the window. + /// The initial size of the window. pub size: (u32, u32), + /// The minimum size of the window. + pub min_size: Option<(u32, u32)>, + + /// The maximum size of the window. + pub max_size: Option<(u32, u32)>, + /// Whether the window should be resizable or not. pub resizable: bool, /// Whether the window should have a border, a title bar, etc. or not. pub decorations: bool, + + /// The icon of the window. + pub icon: Option<Icon>, } impl Default for Settings { fn default() -> Settings { Settings { size: (1024, 768), + min_size: None, + max_size: None, resizable: true, decorations: true, + icon: None, + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From<Settings> for iced_winit::settings::Window { + fn from(settings: Settings) -> Self { + Self { + size: settings.size, + min_size: settings.min_size, + max_size: settings.max_size, + resizable: settings.resizable, + decorations: settings.decorations, + icon: settings.icon.map(Icon::into), + platform_specific: Default::default(), } } } diff --git a/style/src/lib.rs b/style/src/lib.rs index 2c5977b5..72d83aec 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -2,6 +2,8 @@ //! //! It contains a set of styles and stylesheets for most of the built-in //! widgets. +pub use iced_core::{Background, Color}; + pub mod button; pub mod checkbox; pub mod container; diff --git a/web/Cargo.toml b/web/Cargo.toml index 12d3865e..e03d2b63 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -15,10 +15,11 @@ categories = ["web-programming"] maintenance = { status = "actively-developed" } [dependencies] -dodrio = "0.1.0" -wasm-bindgen = "0.2.51" +dodrio = "0.2" +wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" url = "2.0" +num-traits = "0.2" [dependencies.iced_core] version = "0.2" diff --git a/web/src/lib.rs b/web/src/lib.rs index 53b54b7e..b7970c56 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -238,28 +238,25 @@ struct Instance<A: Application> { bus: Bus<A::Message>, } -impl<A> dodrio::Render for Instance<A> +impl<'a, A> dodrio::Render<'a> for Instance<A> where A: Application, { - fn render<'a, 'bump>( - &'a self, - bump: &'bump bumpalo::Bump, - ) -> dodrio::Node<'bump> - where - 'a: 'bump, - { + fn render( + &self, + context: &mut dodrio::RenderContext<'a>, + ) -> dodrio::Node<'a> { use dodrio::builder::*; let mut ui = self.application.borrow_mut(); let element = ui.view(); let mut css = Css::new(); - let node = element.widget.node(bump, &self.bus, &mut css); + let node = element.widget.node(context.bump, &self.bus, &mut css); - div(bump) + div(context.bump) .attr("style", "width: 100%; height: 100%") - .children(vec![css.node(bump), node]) + .children(vec![css.node(context.bump), node]) .finish() } } diff --git a/web/src/widget/button.rs b/web/src/widget/button.rs index 3a5afe60..a4bcc33d 100644 --- a/web/src/widget/button.rs +++ b/web/src/widget/button.rs @@ -154,16 +154,20 @@ where }, }; + let class = { + use dodrio::bumpalo::collections::String; + + String::from_str_in(&padding_class, bump).into_bump_str() + }; + let mut node = button(bump) - .attr( - "class", - bumpalo::format!(in bump, "{}", padding_class).into_bump_str(), - ) + .attr("class", class) .attr( "style", bumpalo::format!( in bump, - "background: {}; border-radius: {}px; width:{}; min-width: {}; color: {}", + "background: {}; border-radius: {}px; width:{}; \ + min-width: {}; color: {}", background, style.border_radius, css::length(self.width), diff --git a/web/src/widget/checkbox.rs b/web/src/widget/checkbox.rs index 5ebc26c8..21801e39 100644 --- a/web/src/widget/checkbox.rs +++ b/web/src/widget/checkbox.rs @@ -28,6 +28,7 @@ pub struct Checkbox<Message> { is_checked: bool, on_toggle: Rc<dyn Fn(bool) -> Message>, label: String, + id: Option<String>, width: Length, style: Box<dyn StyleSheet>, } @@ -51,6 +52,7 @@ impl<Message> Checkbox<Message> { is_checked, on_toggle: Rc::new(f), label: label.into(), + id: None, width: Length::Shrink, style: Default::default(), } @@ -71,6 +73,14 @@ impl<Message> Checkbox<Message> { self.style = style.into(); self } + + /// Sets the id of the [`Checkbox`]. + /// + /// [`Checkbox`]: struct.Checkbox.html + pub fn id(mut self, id: impl Into<String>) -> Self { + self.id = Some(id.into()); + self + } } impl<Message> Widget<Message> for Checkbox<Message> @@ -84,8 +94,10 @@ where style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; + use dodrio::bumpalo::collections::String; - let checkbox_label = bumpalo::format!(in bump, "{}", self.label); + let checkbox_label = + String::from_str_in(&self.label, bump).into_bump_str(); let event_bus = bus.clone(); let on_toggle = self.on_toggle.clone(); @@ -95,7 +107,15 @@ where let spacing_class = style_sheet.insert(bump, css::Rule::Spacing(5)); - label(bump) + let (label, input) = if let Some(id) = &self.id { + let id = String::from_str_in(id, bump).into_bump_str(); + + (label(bump).attr("for", id), input(bump).attr("id", id)) + } else { + (label(bump), input(bump)) + }; + + label .attr( "class", bumpalo::format!(in bump, "{} {}", row_class, spacing_class) @@ -108,7 +128,7 @@ where ) .children(vec![ // TODO: Checkbox styling - input(bump) + input .attr("type", "checkbox") .bool_attr("checked", self.is_checked) .on("click", move |_root, vdom, _event| { @@ -118,8 +138,7 @@ where vdom.schedule_render(); }) .finish(), - span(bump).children(vec![ - text(checkbox_label.into_bump_str())]).finish(), + text(checkbox_label), ]) .finish() } diff --git a/web/src/widget/image.rs b/web/src/widget/image.rs index 029ab352..a595c29a 100644 --- a/web/src/widget/image.rs +++ b/web/src/widget/image.rs @@ -22,6 +22,9 @@ pub struct Image { /// The image path pub handle: Handle, + /// The alt text of the image + pub alt: String, + /// The width of the image pub width: Length, @@ -36,6 +39,7 @@ impl Image { pub fn new<T: Into<Handle>>(handle: T) -> Self { Image { handle: handle.into(), + alt: Default::default(), width: Length::Shrink, height: Length::Shrink, } @@ -56,6 +60,14 @@ impl Image { self.height = height; self } + + /// Sets the alt text of the [`Image`]. + /// + /// [`Image`]: struct.Image.html + pub fn alt(mut self, alt: impl Into<String>) -> Self { + self.alt = alt.into(); + self + } } impl<Message> Widget<Message> for Image { @@ -66,12 +78,19 @@ impl<Message> Widget<Message> for Image { _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; + use dodrio::bumpalo::collections::String; + + let src = String::from_str_in( + match self.handle.data.as_ref() { + Data::Path(path) => path.to_str().unwrap_or(""), + }, + bump, + ) + .into_bump_str(); - let src = bumpalo::format!(in bump, "{}", match self.handle.data.as_ref() { - Data::Path(path) => path.to_str().unwrap_or("") - }); + let alt = String::from_str_in(&self.alt, bump).into_bump_str(); - let mut image = img(bump).attr("src", src.into_bump_str()); + let mut image = img(bump).attr("src", src).attr("alt", alt); match self.width { Length::Shrink => {} diff --git a/web/src/widget/radio.rs b/web/src/widget/radio.rs index 520b24cd..c9d0a00e 100644 --- a/web/src/widget/radio.rs +++ b/web/src/widget/radio.rs @@ -35,6 +35,8 @@ pub struct Radio<Message> { is_selected: bool, on_click: Message, label: String, + id: Option<String>, + name: Option<String>, style: Box<dyn StyleSheet>, } @@ -63,6 +65,8 @@ impl<Message> Radio<Message> { is_selected: Some(value) == selected, on_click: f(value), label: label.into(), + id: None, + name: None, style: Default::default(), } } @@ -74,6 +78,22 @@ impl<Message> Radio<Message> { self.style = style.into(); self } + + /// Sets the name attribute of the [`Radio`] button. + /// + /// [`Radio`]: struct.Radio.html + pub fn name(mut self, name: impl Into<String>) -> Self { + self.name = Some(name.into()); + self + } + + /// Sets the id of the [`Radio`] button. + /// + /// [`Radio`]: struct.Radio.html + pub fn id(mut self, id: impl Into<String>) -> Self { + self.id = Some(id.into()); + self + } } impl<Message> Widget<Message> for Radio<Message> @@ -87,17 +107,35 @@ where _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; + use dodrio::bumpalo::collections::String; - let radio_label = bumpalo::format!(in bump, "{}", self.label); + let radio_label = + String::from_str_in(&self.label, bump).into_bump_str(); let event_bus = bus.clone(); let on_click = self.on_click.clone(); + let (label, input) = if let Some(id) = &self.id { + let id = String::from_str_in(id, bump).into_bump_str(); + + (label(bump).attr("for", id), input(bump).attr("id", id)) + } else { + (label(bump), input(bump)) + }; + + let input = if let Some(name) = &self.name { + let name = String::from_str_in(name, bump).into_bump_str(); + + dodrio::builder::input(bump).attr("name", name) + } else { + input + }; + // TODO: Complete styling - label(bump) + label .attr("style", "display: block; font-size: 20px") .children(vec![ - input(bump) + input .attr("type", "radio") .attr("style", "margin-right: 10px") .bool_attr("checked", self.is_selected) @@ -105,7 +143,7 @@ where event_bus.publish(on_click.clone()); }) .finish(), - text(radio_label.into_bump_str()), + text(radio_label), ]) .finish() } diff --git a/web/src/widget/slider.rs b/web/src/widget/slider.rs index 5aa6439e..a0d9df00 100644 --- a/web/src/widget/slider.rs +++ b/web/src/widget/slider.rs @@ -16,6 +16,9 @@ use std::{ops::RangeInclusive, rc::Rc}; /// /// A [`Slider`] will try to fill the horizontal space of its container. /// +/// The [`Slider`] range of numeric values is generic and its step size defaults +/// to 1 unit. +/// /// [`Slider`]: struct.Slider.html /// /// # Example @@ -34,16 +37,20 @@ use std::{ops::RangeInclusive, rc::Rc}; /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, Message> { +pub struct Slider<'a, T, Message> { _state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, - on_change: Rc<Box<dyn Fn(f32) -> Message>>, + range: RangeInclusive<T>, + step: T, + value: T, + on_change: Rc<Box<dyn Fn(T) -> Message>>, width: Length, style: Box<dyn StyleSheet>, } -impl<'a, Message> Slider<'a, Message> { +impl<'a, T, Message> Slider<'a, T, Message> +where + T: Copy + From<u8> + std::cmp::PartialOrd, +{ /// Creates a new [`Slider`]. /// /// It expects: @@ -58,17 +65,30 @@ impl<'a, Message> Slider<'a, Message> { /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, + range: RangeInclusive<T>, + value: T, on_change: F, ) -> Self where - F: 'static + Fn(f32) -> Message, + F: 'static + Fn(T) -> Message, { + let value = if value >= *range.start() { + value + } else { + *range.start() + }; + + let value = if value <= *range.end() { + value + } else { + *range.end() + }; + Slider { _state: state, - value: value.max(*range.start()).min(*range.end()), + value, range, + step: T::from(1), on_change: Rc::new(Box::new(on_change)), width: Length::Fill, style: Default::default(), @@ -90,10 +110,19 @@ impl<'a, Message> Slider<'a, Message> { self.style = style.into(); self } + + /// Sets the step size of the [`Slider`]. + /// + /// [`Slider`]: struct.Slider.html + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } } -impl<'a, Message> Widget<Message> for Slider<'a, Message> +impl<'a, T, Message> Widget<Message> for Slider<'a, T, Message> where + T: 'static + Copy + Into<f64> + num_traits::FromPrimitive, Message: 'static, { fn node<'b>( @@ -107,18 +136,18 @@ where let (start, end) = self.range.clone().into_inner(); - let min = bumpalo::format!(in bump, "{}", start); - let max = bumpalo::format!(in bump, "{}", end); - let value = bumpalo::format!(in bump, "{}", self.value); + let min = bumpalo::format!(in bump, "{}", start.into()); + let max = bumpalo::format!(in bump, "{}", end.into()); + let value = bumpalo::format!(in bump, "{}", self.value.into()); + let step = bumpalo::format!(in bump, "{}", self.step.into()); let on_change = self.on_change.clone(); let event_bus = bus.clone(); - // TODO: Make `step` configurable // TODO: Styling input(bump) .attr("type", "range") - .attr("step", "0.01") + .attr("step", step.into_bump_str()) .attr("min", min.into_bump_str()) .attr("max", max.into_bump_str()) .attr("value", value.into_bump_str()) @@ -131,19 +160,22 @@ where Some(slider) => slider, }; - if let Ok(value) = slider.value().parse::<f32>() { - event_bus.publish(on_change(value)); + if let Ok(value) = slider.value().parse::<f64>() { + if let Some(value) = T::from_f64(value) { + event_bus.publish(on_change(value)); + } } }) .finish() } } -impl<'a, Message> From<Slider<'a, Message>> for Element<'a, Message> +impl<'a, T, Message> From<Slider<'a, T, Message>> for Element<'a, Message> where + T: 'static + Copy + Into<f64> + num_traits::FromPrimitive, Message: 'static, { - fn from(slider: Slider<'a, Message>) -> Element<'a, Message> { + fn from(slider: Slider<'a, T, Message>) -> Element<'a, Message> { Element::new(slider) } } diff --git a/web/src/widget/text.rs b/web/src/widget/text.rs index 3ec565a8..2f7308ee 100644 --- a/web/src/widget/text.rs +++ b/web/src/widget/text.rs @@ -116,7 +116,12 @@ impl<'a, Message> Widget<Message> for Text { ) -> dodrio::Node<'b> { use dodrio::builder::*; - let content = bumpalo::format!(in bump, "{}", self.content); + let content = { + use dodrio::bumpalo::collections::String; + + String::from_str_in(&self.content, bump) + }; + let color = self .color .map(css::color) @@ -133,7 +138,8 @@ impl<'a, Message> Widget<Message> for Text { let style = bumpalo::format!( in bump, - "width: {}; height: {}; font-size: {}px; color: {}; text-align: {}; font-family: {}", + "width: {}; height: {}; font-size: {}px; color: {}; \ + text-align: {}; font-family: {}", width, height, self.size.unwrap_or(20), diff --git a/web/src/widget/text_input.rs b/web/src/widget/text_input.rs index 3fa458bd..0049a553 100644 --- a/web/src/widget/text_input.rs +++ b/web/src/widget/text_input.rs @@ -151,8 +151,26 @@ where use dodrio::builder::*; use wasm_bindgen::JsCast; - let padding_class = - style_sheet.insert(bump, css::Rule::Padding(self.padding)); + let class = { + use dodrio::bumpalo::collections::String; + + let padding_class = + style_sheet.insert(bump, css::Rule::Padding(self.padding)); + + String::from_str_in(&padding_class, bump).into_bump_str() + }; + + let placeholder = { + use dodrio::bumpalo::collections::String; + + String::from_str_in(&self.placeholder, bump).into_bump_str() + }; + + let value = { + use dodrio::bumpalo::collections::String; + + String::from_str_in(&self.value, bump).into_bump_str() + }; let on_change = self.on_change.clone(); let on_submit = self.on_submit.clone(); @@ -161,15 +179,14 @@ where let style = self.style_sheet.active(); input(bump) - .attr( - "class", - bumpalo::format!(in bump, "{}", padding_class).into_bump_str(), - ) + .attr("class", class) .attr( "style", bumpalo::format!( in bump, - "width: {}; max-width: {}; font-size: {}px; background: {}; border-width: {}px; border-color: {}; border-radius: {}px; color: {}", + "width: {}; max-width: {}; font-size: {}px; \ + background: {}; border-width: {}px; border-color: {}; \ + border-radius: {}px; color: {}", css::length(self.width), css::max_length(self.max_width), self.size.unwrap_or(20), @@ -181,19 +198,9 @@ where ) .into_bump_str(), ) - .attr( - "placeholder", - bumpalo::format!(in bump, "{}", self.placeholder) - .into_bump_str(), - ) - .attr( - "value", - bumpalo::format!(in bump, "{}", self.value).into_bump_str(), - ) - .attr( - "type", - bumpalo::format!(in bump, "{}", if self.is_secure { "password" } else { "text" }).into_bump_str(), - ) + .attr("placeholder", placeholder) + .attr("value", value) + .attr("type", if self.is_secure { "password" } else { "text" }) .on("input", move |_root, _vdom, event| { let text_input = match event.target().and_then(|t| { t.dyn_into::<web_sys::HtmlInputElement>().ok() @@ -206,10 +213,13 @@ where }) .on("keypress", move |_root, _vdom, event| { if let Some(on_submit) = on_submit.clone() { - let event = event.unchecked_into::<web_sys::KeyboardEvent>(); + let event = + event.unchecked_into::<web_sys::KeyboardEvent>(); match event.key_code() { - 13 => { submit_event_bus.publish(on_submit); } + 13 => { + submit_event_bus.publish(on_submit); + } _ => {} } } diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 9ed4438b..a25f42f7 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -24,6 +24,8 @@ pub struct Backend { #[cfg(any(feature = "image", feature = "svg"))] image_pipeline: image::Pipeline, + + default_text_size: u16, } impl Backend { @@ -50,6 +52,8 @@ impl Backend { #[cfg(any(feature = "image", feature = "svg"))] image_pipeline, + + default_text_size: settings.default_text_size, } } @@ -245,6 +249,10 @@ impl backend::Text for Backend { const ICON_FONT: Font = font::ICONS; const CHECKMARK_ICON: char = font::CHECKMARK_ICON; + fn default_size(&self) -> u16 { + self.default_text_size + } + fn measure( &self, contents: &str, diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index e67221c7..e51a225c 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -36,7 +36,7 @@ mod backend; mod quad; mod text; -pub use iced_graphics::{Antialiasing, Defaults, Primitive, Viewport}; +pub use iced_graphics::{Antialiasing, Color, Defaults, Primitive, Viewport}; pub use wgpu; pub use backend::Backend; diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index 4655e64f..bc146c4c 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -16,6 +16,11 @@ pub struct Settings { /// If `None` is provided, a default system font will be chosen. pub default_font: Option<&'static [u8]>, + /// The default size of text. + /// + /// By default, it will be set to 20. + pub default_text_size: u16, + /// The antialiasing strategy that will be used for triangle primitives. pub antialiasing: Option<Antialiasing>, } @@ -25,6 +30,7 @@ impl Default for Settings { Settings { format: wgpu::TextureFormat::Bgra8UnormSrgb, default_font: None, + default_text_size: 20, antialiasing: None, } } diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index fe2388a3..2744c67a 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -16,6 +16,7 @@ const INDEX_BUFFER_SIZE: usize = 10_000; pub(crate) struct Pipeline { pipeline: wgpu::RenderPipeline, blit: Option<msaa::Blit>, + constants_layout: wgpu::BindGroupLayout, constants: wgpu::BindGroup, uniforms_buffer: Buffer<Uniforms>, vertex_buffer: Buffer<Vertex2D>, @@ -50,8 +51,10 @@ impl<T> Buffer<T> { } } - pub fn ensure_capacity(&mut self, device: &wgpu::Device, size: usize) { - if self.size < size { + pub fn expand(&mut self, device: &wgpu::Device, size: usize) -> bool { + let needs_resize = self.size < size; + + if needs_resize { self.raw = device.create_buffer(&wgpu::BufferDescriptor { label: None, size: (std::mem::size_of::<T>() * size) as u64, @@ -60,6 +63,8 @@ impl<T> Buffer<T> { self.size = size; } + + needs_resize } } @@ -69,7 +74,7 @@ impl Pipeline { format: wgpu::TextureFormat, antialiasing: Option<settings::Antialiasing>, ) -> Pipeline { - let constant_layout = + let constants_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: None, bindings: &[wgpu::BindGroupLayoutEntry { @@ -88,7 +93,7 @@ impl Pipeline { let constant_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: None, - layout: &constant_layout, + layout: &constants_layout, bindings: &[wgpu::Binding { binding: 0, resource: wgpu::BindingResource::Buffer { @@ -100,7 +105,7 @@ impl Pipeline { let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - bind_group_layouts: &[&constant_layout], + bind_group_layouts: &[&constants_layout], }); let vs = include_bytes!("shader/triangle.vert.spv"); @@ -180,6 +185,7 @@ impl Pipeline { Pipeline { pipeline, blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), + constants_layout, constants: constant_bind_group, uniforms_buffer: constants_buffer, vertex_buffer: Buffer::new( @@ -220,9 +226,25 @@ impl Pipeline { // Then we ensure the current buffers are big enough, resizing if // necessary - self.uniforms_buffer.ensure_capacity(device, meshes.len()); - self.vertex_buffer.ensure_capacity(device, total_vertices); - self.index_buffer.ensure_capacity(device, total_indices); + let _ = self.vertex_buffer.expand(device, total_vertices); + let _ = self.index_buffer.expand(device, total_indices); + + // If the uniforms buffer is resized, then we need to recreate its + // bind group. + if self.uniforms_buffer.expand(device, meshes.len()) { + self.constants = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &self.constants_layout, + bindings: &[wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::Buffer { + buffer: &self.uniforms_buffer.raw, + range: 0..std::mem::size_of::<Uniforms>() as u64, + }, + }], + }); + } let mut uniforms: Vec<Uniforms> = Vec::with_capacity(meshes.len()); let mut offsets: Vec<( diff --git a/wgpu/src/widget/slider.rs b/wgpu/src/widget/slider.rs index cf036829..3a8c2595 100644 --- a/wgpu/src/widget/slider.rs +++ b/wgpu/src/widget/slider.rs @@ -13,4 +13,4 @@ pub use iced_native::slider::State; /// 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>; +pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Renderer>; diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 8345679a..5bdd34bc 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -1,4 +1,4 @@ -use crate::{Backend, Renderer, Settings}; +use crate::{Backend, Color, Renderer, Settings}; use iced_graphics::Viewport; use iced_native::{futures, mouse}; @@ -103,6 +103,7 @@ impl iced_graphics::window::Compositor for Compositor { renderer: &mut Self::Renderer, swap_chain: &mut Self::SwapChain, viewport: &Viewport, + background_color: Color, output: &<Self::Renderer as iced_native::Renderer>::Output, overlay: &[T], ) -> mouse::Interaction { @@ -118,11 +119,15 @@ impl iced_graphics::window::Compositor for Compositor { resolve_target: None, load_op: wgpu::LoadOp::Clear, store_op: wgpu::StoreOp::Store, - clear_color: wgpu::Color { - r: 1.0, - g: 1.0, - b: 1.0, - a: 1.0, + clear_color: { + let [r, g, b, a] = background_color.into_linear(); + + wgpu::Color { + r: f64::from(r), + g: f64::from(g), + b: f64::from(b), + a: f64::from(a), + } }, }], depth_stencil_attachment: None, diff --git a/winit/src/application.rs b/winit/src/application.rs index 73ac72b2..5b93c8af 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,6 +1,6 @@ //! Create interactive, native cross-platform applications. use crate::{ - conversion, mouse, Clipboard, Command, Debug, Executor, Mode, Proxy, + conversion, mouse, Clipboard, Color, Command, Debug, Executor, Mode, Proxy, Runtime, Settings, Size, Subscription, }; use iced_graphics::window; @@ -73,6 +73,32 @@ pub trait Application: Program { fn mode(&self) -> Mode { Mode::Windowed } + + /// Returns the background [`Color`] of the [`Application`]. + /// + /// By default, it returns [`Color::WHITE`]. + /// + /// [`Color`]: struct.Color.html + /// [`Application`]: trait.Application.html + /// [`Color::WHITE`]: struct.Color.html#const.WHITE + fn background_color(&self) -> Color { + Color::WHITE + } + + /// Returns the scale factor of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + /// + /// [`Application`]: trait.Application.html + fn scale_factor(&self) -> f64 { + 1.0 + } } /// Runs an [`Application`] with an executor, compositor, and the provided @@ -112,6 +138,8 @@ pub fn run<A, E, C>( let mut title = application.title(); let mut mode = application.mode(); + let mut background_color = application.background_color(); + let mut scale_factor = application.scale_factor(); let window = settings .window @@ -120,13 +148,14 @@ pub fn run<A, E, C>( .expect("Open window"); let clipboard = Clipboard::new(&window); + let mut cursor_position = winit::dpi::PhysicalPosition::new(-1.0, -1.0); let mut mouse_interaction = mouse::Interaction::default(); let mut modifiers = winit::event::ModifiersState::default(); let physical_size = window.inner_size(); let mut viewport = Viewport::with_physical_size( Size::new(physical_size.width, physical_size.height), - window.scale_factor(), + window.scale_factor() * scale_factor, ); let mut resized = false; @@ -143,6 +172,7 @@ pub fn run<A, E, C>( let mut state = program::State::new( application, viewport.logical_size(), + conversion::cursor_position(cursor_position, viewport.scale_factor()), &mut renderer, &mut debug, ); @@ -150,10 +180,18 @@ pub fn run<A, E, C>( event_loop.run(move |event, _, control_flow| match event { event::Event::MainEventsCleared => { + if state.is_queue_empty() { + return; + } + let command = runtime.enter(|| { state.update( - clipboard.as_ref().map(|c| c as _), viewport.logical_size(), + conversion::cursor_position( + cursor_position, + viewport.scale_factor(), + ), + clipboard.as_ref().map(|c| c as _), &mut renderer, &mut debug, ) @@ -189,6 +227,39 @@ pub fn run<A, E, C>( mode = new_mode; } + + // Update background color + background_color = program.background_color(); + + // Update scale factor + let new_scale_factor = program.scale_factor(); + + if scale_factor != new_scale_factor { + let size = window.inner_size(); + + viewport = Viewport::with_physical_size( + Size::new(size.width, size.height), + window.scale_factor() * new_scale_factor, + ); + + // We relayout the UI with the new logical size. + // The queue is empty, therefore this will never produce + // a `Command`. + // + // TODO: Properly queue `WindowResized` + let _ = state.update( + viewport.logical_size(), + conversion::cursor_position( + cursor_position, + viewport.scale_factor(), + ), + clipboard.as_ref().map(|c| c as _), + &mut renderer, + &mut debug, + ); + + scale_factor = new_scale_factor; + } } window.request_redraw(); @@ -215,6 +286,7 @@ pub fn run<A, E, C>( &mut renderer, &mut swap_chain, &viewport, + background_color, state.primitive(), &debug.overlay(), ); @@ -239,7 +311,9 @@ pub fn run<A, E, C>( handle_window_event( &window_event, &window, + scale_factor, control_flow, + &mut cursor_position, &mut modifiers, &mut viewport, &mut resized, @@ -266,7 +340,9 @@ pub fn run<A, E, C>( pub fn handle_window_event( event: &winit::event::WindowEvent<'_>, window: &winit::window::Window, + scale_factor: f64, control_flow: &mut winit::event_loop::ControlFlow, + cursor_position: &mut winit::dpi::PhysicalPosition<f64>, modifiers: &mut winit::event::ModifiersState, viewport: &mut Viewport, resized: &mut bool, @@ -278,13 +354,18 @@ pub fn handle_window_event( WindowEvent::Resized(new_size) => { let size = Size::new(new_size.width, new_size.height); - *viewport = - Viewport::with_physical_size(size, window.scale_factor()); + *viewport = Viewport::with_physical_size( + size, + window.scale_factor() * scale_factor, + ); *resized = true; } WindowEvent::CloseRequested => { *control_flow = ControlFlow::Exit; } + WindowEvent::CursorMoved { position, .. } => { + *cursor_position = *position; + } WindowEvent::ModifiersChanged(new_modifiers) => { *modifiers = *new_modifiers; } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index b887db6e..cfa76e88 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -4,7 +4,7 @@ //! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native use crate::{ keyboard::{self, KeyCode, ModifiersState}, - mouse, window, Event, Mode, + mouse, window, Event, Mode, Point, }; /// Converts a winit window event into an iced event. @@ -92,6 +92,9 @@ pub fn window_event( } } })), + WindowEvent::ModifiersChanged(new_modifiers) => Some(Event::Keyboard( + keyboard::Event::ModifiersChanged(modifiers_state(*new_modifiers)), + )), WindowEvent::HoveredFile(path) => { Some(Event::Window(window::Event::FileHovered(path.clone()))) } @@ -174,6 +177,16 @@ pub fn modifiers_state( } } +/// Converts a physical cursor position to a logical `Point`. +pub fn cursor_position( + position: winit::dpi::PhysicalPosition<f64>, + scale_factor: f64, +) -> Point { + let logical_position = position.to_logical(scale_factor); + + Point::new(logical_position.x, logical_position.y) +} + /// Converts a `VirtualKeyCode` from [`winit`] to an [`iced_native`] key code. /// /// [`winit`]: https://github.com/rust-windowing/winit diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 37cb832f..4155bf7d 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -14,7 +14,7 @@ use winit::monitor::MonitorHandle; use winit::window::WindowBuilder; /// The settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Default)] +#[derive(Debug, Clone, Default)] pub struct Settings<Flags> { /// The [`Window`] settings /// @@ -28,17 +28,26 @@ pub struct Settings<Flags> { } /// The window settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct Window { /// The size of the window. pub size: (u32, u32), + /// The minimum size of the window. + pub min_size: Option<(u32, u32)>, + + /// The maximum size of the window. + pub max_size: Option<(u32, u32)>, + /// Whether the window should be resizable or not. pub resizable: bool, /// Whether the window should have a border, a title bar, etc. pub decorations: bool, + /// The window icon, which is also usually used in the taskbar + pub icon: Option<winit::window::Icon>, + /// Platform specific settings. pub platform_specific: platform::PlatformSpecific, } @@ -60,8 +69,19 @@ impl Window { .with_inner_size(winit::dpi::LogicalSize { width, height }) .with_resizable(self.resizable) .with_decorations(self.decorations) + .with_window_icon(self.icon) .with_fullscreen(conversion::fullscreen(primary_monitor, mode)); + if let Some((width, height)) = self.min_size { + window_builder = window_builder + .with_min_inner_size(winit::dpi::LogicalSize { width, height }); + } + + if let Some((width, height)) = self.max_size { + window_builder = window_builder + .with_max_inner_size(winit::dpi::LogicalSize { width, height }); + } + #[cfg(target_os = "windows")] { use winit::platform::windows::WindowBuilderExtWindows; @@ -79,8 +99,11 @@ impl Default for Window { fn default() -> Window { Window { size: (1024, 768), + min_size: None, + max_size: None, resizable: true, decorations: true, + icon: None, platform_specific: Default::default(), } } |