Merge branch 'master' into feature/pane-grid-titlebar

This commit is contained in:
Héctor Ramón Jiménez 2020-07-08 11:44:40 +02:00
commit f3dfaa2c43
59 changed files with 1064 additions and 311 deletions

View File

@ -28,4 +28,7 @@ pub enum Event {
/// A unicode character was received.
CharacterReceived(char),
/// The keyboard modifiers have changed.
ModifiersChanged(ModifiersState),
}

View File

@ -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 {

View File

@ -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)

View File

@ -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 {

View File

@ -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()
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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,
}
}

View File

@ -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>;

View File

@ -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);
}

View File

@ -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,

View File

@ -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.

View File

@ -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,
};

View File

@ -103,7 +103,7 @@ where
} else {
content
},
if is_mouse_over {
if is_mouse_over && !is_disabled {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()

View File

@ -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));

View File

@ -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;

View File

@ -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,

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -13,6 +13,7 @@ debug = []
[dependencies]
twox-hash = "1.5"
unicode-segmentation = "1.6"
num-traits = "0.2"
[dependencies.iced_core]
version = "0.2"

View File

@ -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
}

View File

@ -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());

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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,

View File

@ -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(),

View File

@ -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,

View File

@ -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) {

View File

@ -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,

View File

@ -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};
///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
#[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();
messages.push((self.on_change)(value));
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;
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)
}

View File

@ -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.

View File

@ -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

View File

@ -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")]

View File

@ -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)
}
}

View File

@ -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,
}
}

View File

@ -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;
}

View File

@ -2,5 +2,8 @@
mod mode;
mod settings;
pub mod icon;
pub use icon::Icon;
pub use mode::Mode;
pub use settings::Settings;

132
src/window/icon.rs Normal file
View File

@ -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)
}
}

View File

@ -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(),
}
}
}

View File

@ -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;

View File

@ -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"

View File

@ -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()
}
}

View File

@ -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),

View File

@ -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()
}

View File

@ -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 = bumpalo::format!(in bump, "{}", match self.handle.data.as_ref() {
Data::Path(path) => path.to_str().unwrap_or("")
});
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 mut image = img(bump).attr("src", src.into_bump_str());
let alt = String::from_str_in(&self.alt, bump).into_bump_str();
let mut image = img(bump).attr("src", src).attr("alt", alt);
match self.width {
Length::Shrink => {}

View File

@ -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()
}

View File

@ -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};
///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
#[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)
}
}

View File

@ -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),

View File

@ -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);
}
_ => {}
}
}

View File

@ -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,

View File

@ -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;

View File

@ -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,
}
}

View File

@ -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<(

View File

@ -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>;

View File

@ -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,

View File

@ -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;
}

View File

@ -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

View File

@ -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(),
}
}