summaryrefslogtreecommitdiffstats
path: root/examples
diff options
context:
space:
mode:
Diffstat (limited to 'examples')
-rw-r--r--examples/custom_quad/src/main.rs11
-rw-r--r--examples/custom_shader/Cargo.toml17
-rw-r--r--examples/custom_shader/src/main.rs163
-rw-r--r--examples/custom_shader/src/scene.rs186
-rw-r--r--examples/custom_shader/src/scene/camera.rs53
-rw-r--r--examples/custom_shader/src/scene/pipeline.rs621
-rw-r--r--examples/custom_shader/src/scene/pipeline/buffer.rs41
-rw-r--r--examples/custom_shader/src/scene/pipeline/cube.rs326
-rw-r--r--examples/custom_shader/src/scene/pipeline/uniforms.rs23
-rw-r--r--examples/custom_shader/src/scene/pipeline/vertex.rs31
-rw-r--r--examples/custom_shader/src/shaders/cubes.wgsl123
-rw-r--r--examples/custom_shader/src/shaders/depth.wgsl48
-rw-r--r--examples/custom_shader/textures/ice_cube_normal_map.pngbin0 -> 1773656 bytes
-rw-r--r--examples/custom_shader/textures/skybox/neg_x.jpgbin0 -> 7549 bytes
-rw-r--r--examples/custom_shader/textures/skybox/neg_y.jpgbin0 -> 2722 bytes
-rw-r--r--examples/custom_shader/textures/skybox/neg_z.jpgbin0 -> 3986 bytes
-rw-r--r--examples/custom_shader/textures/skybox/pos_x.jpgbin0 -> 5522 bytes
-rw-r--r--examples/custom_shader/textures/skybox/pos_y.jpgbin0 -> 3382 bytes
-rw-r--r--examples/custom_shader/textures/skybox/pos_z.jpgbin0 -> 5205 bytes
-rw-r--r--examples/custom_widget/src/main.rs11
-rw-r--r--examples/download_progress/src/main.rs19
-rw-r--r--examples/editor/Cargo.toml15
-rw-r--r--examples/editor/fonts/icons.ttfbin0 -> 6352 bytes
-rw-r--r--examples/editor/src/main.rs312
-rw-r--r--examples/events/src/main.rs15
-rw-r--r--examples/exit/src/main.rs2
-rw-r--r--examples/game_of_life/Cargo.toml2
-rw-r--r--examples/game_of_life/src/main.rs4
-rw-r--r--examples/geometry/src/main.rs15
-rw-r--r--examples/integration/src/controls.rs39
-rw-r--r--examples/integration/src/main.rs142
-rw-r--r--examples/integration/src/scene.rs4
-rw-r--r--examples/layout/Cargo.toml9
-rw-r--r--examples/layout/src/main.rs371
-rw-r--r--examples/lazy/src/main.rs48
-rw-r--r--examples/loading_spinners/src/circular.rs24
-rw-r--r--examples/loading_spinners/src/linear.rs24
-rw-r--r--examples/loading_spinners/src/main.rs11
-rw-r--r--examples/modal/src/main.rs32
-rw-r--r--examples/multi_window/Cargo.toml9
-rw-r--r--examples/multi_window/src/main.rs215
-rw-r--r--examples/pane_grid/src/main.rs32
-rw-r--r--examples/pick_list/src/main.rs9
-rw-r--r--examples/progress_bar/README.md2
-rw-r--r--examples/screenshot/src/main.rs41
-rw-r--r--examples/scrollable/src/main.rs52
-rw-r--r--examples/sierpinski_triangle/src/main.rs2
-rw-r--r--examples/solar_system/src/main.rs14
-rw-r--r--examples/stopwatch/src/main.rs12
-rw-r--r--examples/styling/src/main.rs26
-rw-r--r--examples/svg/src/main.rs1
-rw-r--r--examples/toast/src/main.rs36
-rw-r--r--examples/todos/src/main.rs39
-rw-r--r--examples/tour/src/main.rs51
-rw-r--r--examples/vectorial_text/Cargo.toml9
-rw-r--r--examples/vectorial_text/src/main.rs175
-rw-r--r--examples/visible_bounds/src/main.rs2
-rw-r--r--examples/websocket/Cargo.toml2
-rw-r--r--examples/websocket/src/main.rs15
59 files changed, 3101 insertions, 385 deletions
diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs
index 13b08250..cc9ad528 100644
--- a/examples/custom_quad/src/main.rs
+++ b/examples/custom_quad/src/main.rs
@@ -26,12 +26,11 @@ mod quad {
where
Renderer: renderer::Renderer,
{
- fn width(&self) -> Length {
- Length::Shrink
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Shrink,
+ height: Length::Shrink,
+ }
}
fn layout(
diff --git a/examples/custom_shader/Cargo.toml b/examples/custom_shader/Cargo.toml
new file mode 100644
index 00000000..b602f98d
--- /dev/null
+++ b/examples/custom_shader/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "custom_shader"
+version = "0.1.0"
+authors = ["Bingus <shankern@protonmail.com>"]
+edition = "2021"
+
+[dependencies]
+iced.workspace = true
+iced.features = ["debug", "advanced"]
+
+image.workspace = true
+bytemuck.workspace = true
+
+glam.workspace = true
+glam.features = ["bytemuck"]
+
+rand = "0.8.5"
diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs
new file mode 100644
index 00000000..3bfa3a43
--- /dev/null
+++ b/examples/custom_shader/src/main.rs
@@ -0,0 +1,163 @@
+mod scene;
+
+use scene::Scene;
+
+use iced::executor;
+use iced::time::Instant;
+use iced::widget::shader::wgpu;
+use iced::widget::{checkbox, column, container, row, shader, slider, text};
+use iced::window;
+use iced::{
+ Alignment, Application, Color, Command, Element, Length, Renderer,
+ Subscription, Theme,
+};
+
+fn main() -> iced::Result {
+ IcedCubes::run(iced::Settings::default())
+}
+
+struct IcedCubes {
+ start: Instant,
+ scene: Scene,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ CubeAmountChanged(u32),
+ CubeSizeChanged(f32),
+ Tick(Instant),
+ ShowDepthBuffer(bool),
+ LightColorChanged(Color),
+}
+
+impl Application for IcedCubes {
+ type Executor = executor::Default;
+ type Message = Message;
+ type Theme = Theme;
+ type Flags = ();
+
+ fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
+ (
+ Self {
+ start: Instant::now(),
+ scene: Scene::new(),
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ "Iced Cubes".to_string()
+ }
+
+ fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
+ match message {
+ Message::CubeAmountChanged(amount) => {
+ self.scene.change_amount(amount);
+ }
+ Message::CubeSizeChanged(size) => {
+ self.scene.size = size;
+ }
+ Message::Tick(time) => {
+ self.scene.update(time - self.start);
+ }
+ Message::ShowDepthBuffer(show) => {
+ self.scene.show_depth_buffer = show;
+ }
+ Message::LightColorChanged(color) => {
+ self.scene.light_color = color;
+ }
+ }
+
+ Command::none()
+ }
+
+ fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> {
+ let top_controls = row![
+ control(
+ "Amount",
+ slider(
+ 1..=scene::MAX,
+ self.scene.cubes.len() as u32,
+ Message::CubeAmountChanged
+ )
+ .width(100)
+ ),
+ control(
+ "Size",
+ slider(0.1..=0.25, self.scene.size, Message::CubeSizeChanged)
+ .step(0.01)
+ .width(100),
+ ),
+ checkbox(
+ "Show Depth Buffer",
+ self.scene.show_depth_buffer,
+ Message::ShowDepthBuffer
+ ),
+ ]
+ .spacing(40);
+
+ let bottom_controls = row![
+ control(
+ "R",
+ slider(0.0..=1.0, self.scene.light_color.r, move |r| {
+ Message::LightColorChanged(Color {
+ r,
+ ..self.scene.light_color
+ })
+ })
+ .step(0.01)
+ .width(100)
+ ),
+ control(
+ "G",
+ slider(0.0..=1.0, self.scene.light_color.g, move |g| {
+ Message::LightColorChanged(Color {
+ g,
+ ..self.scene.light_color
+ })
+ })
+ .step(0.01)
+ .width(100)
+ ),
+ control(
+ "B",
+ slider(0.0..=1.0, self.scene.light_color.b, move |b| {
+ Message::LightColorChanged(Color {
+ b,
+ ..self.scene.light_color
+ })
+ })
+ .step(0.01)
+ .width(100)
+ )
+ ]
+ .spacing(40);
+
+ let controls = column![top_controls, bottom_controls,]
+ .spacing(10)
+ .padding(20)
+ .align_items(Alignment::Center);
+
+ let shader =
+ shader(&self.scene).width(Length::Fill).height(Length::Fill);
+
+ container(column![shader, controls].align_items(Alignment::Center))
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+ }
+
+ fn subscription(&self) -> Subscription<Self::Message> {
+ window::frames().map(Message::Tick)
+ }
+}
+
+fn control<'a>(
+ label: &'static str,
+ control: impl Into<Element<'a, Message>>,
+) -> Element<'a, Message> {
+ row![text(label), control.into()].spacing(10).into()
+}
diff --git a/examples/custom_shader/src/scene.rs b/examples/custom_shader/src/scene.rs
new file mode 100644
index 00000000..a35efdd9
--- /dev/null
+++ b/examples/custom_shader/src/scene.rs
@@ -0,0 +1,186 @@
+mod camera;
+mod pipeline;
+
+use camera::Camera;
+use pipeline::Pipeline;
+
+use crate::wgpu;
+use pipeline::cube::{self, Cube};
+
+use iced::mouse;
+use iced::time::Duration;
+use iced::widget::shader;
+use iced::{Color, Rectangle, Size};
+
+use glam::Vec3;
+use rand::Rng;
+use std::cmp::Ordering;
+use std::iter;
+
+pub const MAX: u32 = 500;
+
+#[derive(Clone)]
+pub struct Scene {
+ pub size: f32,
+ pub cubes: Vec<Cube>,
+ pub camera: Camera,
+ pub show_depth_buffer: bool,
+ pub light_color: Color,
+}
+
+impl Scene {
+ pub fn new() -> Self {
+ let mut scene = Self {
+ size: 0.2,
+ cubes: vec![],
+ camera: Camera::default(),
+ show_depth_buffer: false,
+ light_color: Color::WHITE,
+ };
+
+ scene.change_amount(MAX);
+
+ scene
+ }
+
+ pub fn update(&mut self, time: Duration) {
+ for cube in self.cubes.iter_mut() {
+ cube.update(self.size, time.as_secs_f32());
+ }
+ }
+
+ pub fn change_amount(&mut self, amount: u32) {
+ let curr_cubes = self.cubes.len() as u32;
+
+ match amount.cmp(&curr_cubes) {
+ Ordering::Greater => {
+ // spawn
+ let cubes_2_spawn = (amount - curr_cubes) as usize;
+
+ let mut cubes = 0;
+ self.cubes.extend(iter::from_fn(|| {
+ if cubes < cubes_2_spawn {
+ cubes += 1;
+ Some(Cube::new(self.size, rnd_origin()))
+ } else {
+ None
+ }
+ }));
+ }
+ Ordering::Less => {
+ // chop
+ let cubes_2_cut = curr_cubes - amount;
+ let new_len = self.cubes.len() - cubes_2_cut as usize;
+ self.cubes.truncate(new_len);
+ }
+ Ordering::Equal => {}
+ }
+ }
+}
+
+impl<Message> shader::Program<Message> for Scene {
+ type State = ();
+ type Primitive = Primitive;
+
+ fn draw(
+ &self,
+ _state: &Self::State,
+ _cursor: mouse::Cursor,
+ bounds: Rectangle,
+ ) -> Self::Primitive {
+ Primitive::new(
+ &self.cubes,
+ &self.camera,
+ bounds,
+ self.show_depth_buffer,
+ self.light_color,
+ )
+ }
+}
+
+/// A collection of `Cube`s that can be rendered.
+#[derive(Debug)]
+pub struct Primitive {
+ cubes: Vec<cube::Raw>,
+ uniforms: pipeline::Uniforms,
+ show_depth_buffer: bool,
+}
+
+impl Primitive {
+ pub fn new(
+ cubes: &[Cube],
+ camera: &Camera,
+ bounds: Rectangle,
+ show_depth_buffer: bool,
+ light_color: Color,
+ ) -> Self {
+ let uniforms = pipeline::Uniforms::new(camera, bounds, light_color);
+
+ Self {
+ cubes: cubes
+ .iter()
+ .map(cube::Raw::from_cube)
+ .collect::<Vec<cube::Raw>>(),
+ uniforms,
+ show_depth_buffer,
+ }
+ }
+}
+
+impl shader::Primitive for Primitive {
+ fn prepare(
+ &self,
+ format: wgpu::TextureFormat,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ _bounds: Rectangle,
+ target_size: Size<u32>,
+ _scale_factor: f32,
+ storage: &mut shader::Storage,
+ ) {
+ if !storage.has::<Pipeline>() {
+ storage.store(Pipeline::new(device, queue, format, target_size));
+ }
+
+ let pipeline = storage.get_mut::<Pipeline>().unwrap();
+
+ //upload data to GPU
+ pipeline.update(
+ device,
+ queue,
+ target_size,
+ &self.uniforms,
+ self.cubes.len(),
+ &self.cubes,
+ );
+ }
+
+ fn render(
+ &self,
+ storage: &shader::Storage,
+ target: &wgpu::TextureView,
+ _target_size: Size<u32>,
+ viewport: Rectangle<u32>,
+ encoder: &mut wgpu::CommandEncoder,
+ ) {
+ //at this point our pipeline should always be initialized
+ let pipeline = storage.get::<Pipeline>().unwrap();
+
+ //render primitive
+ pipeline.render(
+ target,
+ encoder,
+ viewport,
+ self.cubes.len() as u32,
+ self.show_depth_buffer,
+ );
+ }
+}
+
+fn rnd_origin() -> Vec3 {
+ Vec3::new(
+ rand::thread_rng().gen_range(-4.0..4.0),
+ rand::thread_rng().gen_range(-4.0..4.0),
+ rand::thread_rng().gen_range(-4.0..2.0),
+ )
+}
diff --git a/examples/custom_shader/src/scene/camera.rs b/examples/custom_shader/src/scene/camera.rs
new file mode 100644
index 00000000..2a49c102
--- /dev/null
+++ b/examples/custom_shader/src/scene/camera.rs
@@ -0,0 +1,53 @@
+use glam::{mat4, vec3, vec4};
+use iced::Rectangle;
+
+#[derive(Copy, Clone)]
+pub struct Camera {
+ eye: glam::Vec3,
+ target: glam::Vec3,
+ up: glam::Vec3,
+ fov_y: f32,
+ near: f32,
+ far: f32,
+}
+
+impl Default for Camera {
+ fn default() -> Self {
+ Self {
+ eye: vec3(0.0, 2.0, 3.0),
+ target: glam::Vec3::ZERO,
+ up: glam::Vec3::Y,
+ fov_y: 45.0,
+ near: 0.1,
+ far: 100.0,
+ }
+ }
+}
+
+pub const OPENGL_TO_WGPU_MATRIX: glam::Mat4 = mat4(
+ vec4(1.0, 0.0, 0.0, 0.0),
+ vec4(0.0, 1.0, 0.0, 0.0),
+ vec4(0.0, 0.0, 0.5, 0.0),
+ vec4(0.0, 0.0, 0.5, 1.0),
+);
+
+impl Camera {
+ pub fn build_view_proj_matrix(&self, bounds: Rectangle) -> glam::Mat4 {
+ //TODO looks distorted without padding; base on surface texture size instead?
+ let aspect_ratio = bounds.width / (bounds.height + 150.0);
+
+ let view = glam::Mat4::look_at_rh(self.eye, self.target, self.up);
+ let proj = glam::Mat4::perspective_rh(
+ self.fov_y,
+ aspect_ratio,
+ self.near,
+ self.far,
+ );
+
+ OPENGL_TO_WGPU_MATRIX * proj * view
+ }
+
+ pub fn position(&self) -> glam::Vec4 {
+ glam::Vec4::from((self.eye, 0.0))
+ }
+}
diff --git a/examples/custom_shader/src/scene/pipeline.rs b/examples/custom_shader/src/scene/pipeline.rs
new file mode 100644
index 00000000..50b70a98
--- /dev/null
+++ b/examples/custom_shader/src/scene/pipeline.rs
@@ -0,0 +1,621 @@
+pub mod cube;
+
+mod buffer;
+mod uniforms;
+mod vertex;
+
+pub use uniforms::Uniforms;
+
+use buffer::Buffer;
+use vertex::Vertex;
+
+use crate::wgpu;
+use crate::wgpu::util::DeviceExt;
+
+use iced::{Rectangle, Size};
+
+const SKY_TEXTURE_SIZE: u32 = 128;
+
+pub struct Pipeline {
+ pipeline: wgpu::RenderPipeline,
+ vertices: wgpu::Buffer,
+ cubes: Buffer,
+ uniforms: wgpu::Buffer,
+ uniform_bind_group: wgpu::BindGroup,
+ depth_texture_size: Size<u32>,
+ depth_view: wgpu::TextureView,
+ depth_pipeline: DepthPipeline,
+}
+
+impl Pipeline {
+ pub fn new(
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ format: wgpu::TextureFormat,
+ target_size: Size<u32>,
+ ) -> Self {
+ //vertices of one cube
+ let vertices =
+ device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("cubes vertex buffer"),
+ contents: bytemuck::cast_slice(&cube::Raw::vertices()),
+ usage: wgpu::BufferUsages::VERTEX,
+ });
+
+ //cube instance data
+ let cubes_buffer = Buffer::new(
+ device,
+ "cubes instance buffer",
+ std::mem::size_of::<cube::Raw>() as u64,
+ wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ );
+
+ //uniforms for all cubes
+ let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("cubes uniform buffer"),
+ size: std::mem::size_of::<Uniforms>() as u64,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ //depth buffer
+ let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("cubes depth texture"),
+ size: wgpu::Extent3d {
+ width: target_size.width,
+ height: target_size.height,
+ depth_or_array_layers: 1,
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Depth32Float,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
+ | wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ });
+
+ let depth_view =
+ depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ let normal_map_data = load_normal_map_data();
+
+ //normal map
+ let normal_texture = device.create_texture_with_data(
+ queue,
+ &wgpu::TextureDescriptor {
+ label: Some("cubes normal map texture"),
+ size: wgpu::Extent3d {
+ width: 1024,
+ height: 1024,
+ depth_or_array_layers: 1,
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Rgba8Unorm,
+ usage: wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ },
+ wgpu::util::TextureDataOrder::LayerMajor,
+ &normal_map_data,
+ );
+
+ let normal_view =
+ normal_texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ //skybox texture for reflection/refraction
+ let skybox_data = load_skybox_data();
+
+ let skybox_texture = device.create_texture_with_data(
+ queue,
+ &wgpu::TextureDescriptor {
+ label: Some("cubes skybox texture"),
+ size: wgpu::Extent3d {
+ width: SKY_TEXTURE_SIZE,
+ height: SKY_TEXTURE_SIZE,
+ depth_or_array_layers: 6, //one for each face of the cube
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Rgba8Unorm,
+ usage: wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ },
+ wgpu::util::TextureDataOrder::LayerMajor,
+ &skybox_data,
+ );
+
+ let sky_view =
+ skybox_texture.create_view(&wgpu::TextureViewDescriptor {
+ label: Some("cubes skybox texture view"),
+ dimension: Some(wgpu::TextureViewDimension::Cube),
+ ..Default::default()
+ });
+
+ let sky_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("cubes skybox sampler"),
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
+ address_mode_w: wgpu::AddressMode::ClampToEdge,
+ mag_filter: wgpu::FilterMode::Linear,
+ min_filter: wgpu::FilterMode::Linear,
+ mipmap_filter: wgpu::FilterMode::Linear,
+ ..Default::default()
+ });
+
+ let uniform_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("cubes uniform bind group layout"),
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float {
+ filterable: true,
+ },
+ view_dimension: wgpu::TextureViewDimension::Cube,
+ multisampled: false,
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 2,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(
+ wgpu::SamplerBindingType::Filtering,
+ ),
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 3,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float {
+ filterable: true,
+ },
+ view_dimension: wgpu::TextureViewDimension::D2,
+ multisampled: false,
+ },
+ count: None,
+ },
+ ],
+ });
+
+ let uniform_bind_group =
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("cubes uniform bind group"),
+ layout: &uniform_bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: uniforms.as_entire_binding(),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::TextureView(&sky_view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 2,
+ resource: wgpu::BindingResource::Sampler(&sky_sampler),
+ },
+ wgpu::BindGroupEntry {
+ binding: 3,
+ resource: wgpu::BindingResource::TextureView(
+ &normal_view,
+ ),
+ },
+ ],
+ });
+
+ let layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("cubes pipeline layout"),
+ bind_group_layouts: &[&uniform_bind_group_layout],
+ push_constant_ranges: &[],
+ });
+
+ let shader =
+ device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("cubes shader"),
+ source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
+ include_str!("../shaders/cubes.wgsl"),
+ )),
+ });
+
+ let pipeline =
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("cubes pipeline"),
+ layout: Some(&layout),
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "vs_main",
+ buffers: &[Vertex::desc(), cube::Raw::desc()],
+ },
+ primitive: wgpu::PrimitiveState::default(),
+ depth_stencil: Some(wgpu::DepthStencilState {
+ format: wgpu::TextureFormat::Depth32Float,
+ depth_write_enabled: true,
+ depth_compare: wgpu::CompareFunction::Less,
+ stencil: wgpu::StencilState::default(),
+ bias: wgpu::DepthBiasState::default(),
+ }),
+ multisample: wgpu::MultisampleState {
+ count: 1,
+ mask: !0,
+ alpha_to_coverage_enabled: false,
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "fs_main",
+ targets: &[Some(wgpu::ColorTargetState {
+ format,
+ blend: Some(wgpu::BlendState {
+ color: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::SrcAlpha,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
+ },
+ alpha: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::One,
+ dst_factor: wgpu::BlendFactor::One,
+ operation: wgpu::BlendOperation::Max,
+ },
+ }),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ }),
+ multiview: None,
+ });
+
+ let depth_pipeline = DepthPipeline::new(
+ device,
+ format,
+ depth_texture.create_view(&wgpu::TextureViewDescriptor::default()),
+ );
+
+ Self {
+ pipeline,
+ cubes: cubes_buffer,
+ uniforms,
+ uniform_bind_group,
+ vertices,
+ depth_texture_size: target_size,
+ depth_view,
+ depth_pipeline,
+ }
+ }
+
+ fn update_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) {
+ if self.depth_texture_size.height != size.height
+ || self.depth_texture_size.width != size.width
+ {
+ let text = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("cubes depth texture"),
+ size: wgpu::Extent3d {
+ width: size.width,
+ height: size.height,
+ depth_or_array_layers: 1,
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Depth32Float,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
+ | wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ });
+
+ self.depth_view =
+ text.create_view(&wgpu::TextureViewDescriptor::default());
+ self.depth_texture_size = size;
+
+ self.depth_pipeline.update(device, &text);
+ }
+ }
+
+ pub fn update(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ target_size: Size<u32>,
+ uniforms: &Uniforms,
+ num_cubes: usize,
+ cubes: &[cube::Raw],
+ ) {
+ //recreate depth texture if surface texture size has changed
+ self.update_depth_texture(device, target_size);
+
+ // update uniforms
+ queue.write_buffer(&self.uniforms, 0, bytemuck::bytes_of(uniforms));
+
+ //resize cubes vertex buffer if cubes amount changed
+ let new_size = num_cubes * std::mem::size_of::<cube::Raw>();
+ self.cubes.resize(device, new_size as u64);
+
+ //always write new cube data since they are constantly rotating
+ queue.write_buffer(&self.cubes.raw, 0, bytemuck::cast_slice(cubes));
+ }
+
+ pub fn render(
+ &self,
+ target: &wgpu::TextureView,
+ encoder: &mut wgpu::CommandEncoder,
+ viewport: Rectangle<u32>,
+ num_cubes: u32,
+ show_depth: bool,
+ ) {
+ {
+ let mut pass =
+ encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("cubes.pipeline.pass"),
+ color_attachments: &[Some(
+ wgpu::RenderPassColorAttachment {
+ view: target,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: wgpu::StoreOp::Store,
+ },
+ },
+ )],
+ depth_stencil_attachment: Some(
+ wgpu::RenderPassDepthStencilAttachment {
+ view: &self.depth_view,
+ depth_ops: Some(wgpu::Operations {
+ load: wgpu::LoadOp::Clear(1.0),
+ store: wgpu::StoreOp::Store,
+ }),
+ stencil_ops: None,
+ },
+ ),
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ });
+
+ pass.set_scissor_rect(
+ viewport.x,
+ viewport.y,
+ viewport.width,
+ viewport.height,
+ );
+ pass.set_pipeline(&self.pipeline);
+ pass.set_bind_group(0, &self.uniform_bind_group, &[]);
+ pass.set_vertex_buffer(0, self.vertices.slice(..));
+ pass.set_vertex_buffer(1, self.cubes.raw.slice(..));
+ pass.draw(0..36, 0..num_cubes);
+ }
+
+ if show_depth {
+ self.depth_pipeline.render(encoder, target, viewport);
+ }
+ }
+}
+
+struct DepthPipeline {
+ pipeline: wgpu::RenderPipeline,
+ bind_group_layout: wgpu::BindGroupLayout,
+ bind_group: wgpu::BindGroup,
+ sampler: wgpu::Sampler,
+ depth_view: wgpu::TextureView,
+}
+
+impl DepthPipeline {
+ pub fn new(
+ device: &wgpu::Device,
+ format: wgpu::TextureFormat,
+ depth_texture: wgpu::TextureView,
+ ) -> Self {
+ let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("cubes.depth_pipeline.sampler"),
+ ..Default::default()
+ });
+
+ let bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("cubes.depth_pipeline.bind_group_layout"),
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(
+ wgpu::SamplerBindingType::NonFiltering,
+ ),
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float {
+ filterable: false,
+ },
+ view_dimension: wgpu::TextureViewDimension::D2,
+ multisampled: false,
+ },
+ count: None,
+ },
+ ],
+ });
+
+ let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("cubes.depth_pipeline.bind_group"),
+ layout: &bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Sampler(&sampler),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::TextureView(
+ &depth_texture,
+ ),
+ },
+ ],
+ });
+
+ let layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("cubes.depth_pipeline.layout"),
+ bind_group_layouts: &[&bind_group_layout],
+ push_constant_ranges: &[],
+ });
+
+ let shader =
+ device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("cubes.depth_pipeline.shader"),
+ source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
+ include_str!("../shaders/depth.wgsl"),
+ )),
+ });
+
+ let pipeline =
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("cubes.depth_pipeline.pipeline"),
+ layout: Some(&layout),
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "vs_main",
+ buffers: &[],
+ },
+ primitive: wgpu::PrimitiveState::default(),
+ depth_stencil: Some(wgpu::DepthStencilState {
+ format: wgpu::TextureFormat::Depth32Float,
+ depth_write_enabled: false,
+ depth_compare: wgpu::CompareFunction::Less,
+ stencil: wgpu::StencilState::default(),
+ bias: wgpu::DepthBiasState::default(),
+ }),
+ multisample: wgpu::MultisampleState::default(),
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "fs_main",
+ targets: &[Some(wgpu::ColorTargetState {
+ format,
+ blend: Some(wgpu::BlendState::REPLACE),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ }),
+ multiview: None,
+ });
+
+ Self {
+ pipeline,
+ bind_group_layout,
+ bind_group,
+ sampler,
+ depth_view: depth_texture,
+ }
+ }
+
+ pub fn update(
+ &mut self,
+ device: &wgpu::Device,
+ depth_texture: &wgpu::Texture,
+ ) {
+ self.depth_view =
+ depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ self.bind_group =
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("cubes.depth_pipeline.bind_group"),
+ layout: &self.bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Sampler(&self.sampler),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::TextureView(
+ &self.depth_view,
+ ),
+ },
+ ],
+ });
+ }
+
+ pub fn render(
+ &self,
+ encoder: &mut wgpu::CommandEncoder,
+ target: &wgpu::TextureView,
+ viewport: Rectangle<u32>,
+ ) {
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("cubes.pipeline.depth_pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: target,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: Some(
+ wgpu::RenderPassDepthStencilAttachment {
+ view: &self.depth_view,
+ depth_ops: None,
+ stencil_ops: None,
+ },
+ ),
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ });
+
+ pass.set_scissor_rect(
+ viewport.x,
+ viewport.y,
+ viewport.width,
+ viewport.height,
+ );
+ pass.set_pipeline(&self.pipeline);
+ pass.set_bind_group(0, &self.bind_group, &[]);
+ pass.draw(0..6, 0..1);
+ }
+}
+
+fn load_skybox_data() -> Vec<u8> {
+ let pos_x: &[u8] = include_bytes!("../../textures/skybox/pos_x.jpg");
+ let neg_x: &[u8] = include_bytes!("../../textures/skybox/neg_x.jpg");
+ let pos_y: &[u8] = include_bytes!("../../textures/skybox/pos_y.jpg");
+ let neg_y: &[u8] = include_bytes!("../../textures/skybox/neg_y.jpg");
+ let pos_z: &[u8] = include_bytes!("../../textures/skybox/pos_z.jpg");
+ let neg_z: &[u8] = include_bytes!("../../textures/skybox/neg_z.jpg");
+
+ let data: [&[u8]; 6] = [pos_x, neg_x, pos_y, neg_y, pos_z, neg_z];
+
+ data.iter().fold(vec![], |mut acc, bytes| {
+ let i = image::load_from_memory_with_format(
+ bytes,
+ image::ImageFormat::Jpeg,
+ )
+ .unwrap()
+ .to_rgba8()
+ .into_raw();
+
+ acc.extend(i);
+ acc
+ })
+}
+
+fn load_normal_map_data() -> Vec<u8> {
+ let bytes: &[u8] = include_bytes!("../../textures/ice_cube_normal_map.png");
+
+ image::load_from_memory_with_format(bytes, image::ImageFormat::Png)
+ .unwrap()
+ .to_rgba8()
+ .into_raw()
+}
diff --git a/examples/custom_shader/src/scene/pipeline/buffer.rs b/examples/custom_shader/src/scene/pipeline/buffer.rs
new file mode 100644
index 00000000..ef4c41c9
--- /dev/null
+++ b/examples/custom_shader/src/scene/pipeline/buffer.rs
@@ -0,0 +1,41 @@
+use crate::wgpu;
+
+// A custom buffer container for dynamic resizing.
+pub struct Buffer {
+ pub raw: wgpu::Buffer,
+ label: &'static str,
+ size: u64,
+ usage: wgpu::BufferUsages,
+}
+
+impl Buffer {
+ pub fn new(
+ device: &wgpu::Device,
+ label: &'static str,
+ size: u64,
+ usage: wgpu::BufferUsages,
+ ) -> Self {
+ Self {
+ raw: device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some(label),
+ size,
+ usage,
+ mapped_at_creation: false,
+ }),
+ label,
+ size,
+ usage,
+ }
+ }
+
+ pub fn resize(&mut self, device: &wgpu::Device, new_size: u64) {
+ if new_size > self.size {
+ self.raw = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some(self.label),
+ size: new_size,
+ usage: self.usage,
+ mapped_at_creation: false,
+ });
+ }
+ }
+}
diff --git a/examples/custom_shader/src/scene/pipeline/cube.rs b/examples/custom_shader/src/scene/pipeline/cube.rs
new file mode 100644
index 00000000..de8bad6c
--- /dev/null
+++ b/examples/custom_shader/src/scene/pipeline/cube.rs
@@ -0,0 +1,326 @@
+use crate::scene::pipeline::Vertex;
+use crate::wgpu;
+
+use glam::{vec2, vec3, Vec3};
+use rand::{thread_rng, Rng};
+
+/// A single instance of a cube.
+#[derive(Debug, Clone)]
+pub struct Cube {
+ pub rotation: glam::Quat,
+ pub position: Vec3,
+ pub size: f32,
+ rotation_dir: f32,
+ rotation_axis: glam::Vec3,
+}
+
+impl Default for Cube {
+ fn default() -> Self {
+ Self {
+ rotation: glam::Quat::IDENTITY,
+ position: glam::Vec3::ZERO,
+ size: 0.1,
+ rotation_dir: 1.0,
+ rotation_axis: glam::Vec3::Y,
+ }
+ }
+}
+
+impl Cube {
+ pub fn new(size: f32, origin: Vec3) -> Self {
+ let rnd = thread_rng().gen_range(0.0..=1.0f32);
+
+ Self {
+ rotation: glam::Quat::IDENTITY,
+ position: origin + Vec3::new(0.1, 0.1, 0.1),
+ size,
+ rotation_dir: if rnd <= 0.5 { -1.0 } else { 1.0 },
+ rotation_axis: if rnd <= 0.33 {
+ glam::Vec3::Y
+ } else if rnd <= 0.66 {
+ glam::Vec3::X
+ } else {
+ glam::Vec3::Z
+ },
+ }
+ }
+
+ pub fn update(&mut self, size: f32, time: f32) {
+ self.rotation = glam::Quat::from_axis_angle(
+ self.rotation_axis,
+ time / 2.0 * self.rotation_dir,
+ );
+ self.size = size;
+ }
+}
+
+#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug)]
+#[repr(C)]
+pub struct Raw {
+ transformation: glam::Mat4,
+ normal: glam::Mat3,
+ _padding: [f32; 3],
+}
+
+impl Raw {
+ const ATTRIBS: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![
+ //cube transformation matrix
+ 4 => Float32x4,
+ 5 => Float32x4,
+ 6 => Float32x4,
+ 7 => Float32x4,
+ //normal rotation matrix
+ 8 => Float32x3,
+ 9 => Float32x3,
+ 10 => Float32x3,
+ ];
+
+ pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
+ wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
+ step_mode: wgpu::VertexStepMode::Instance,
+ attributes: &Self::ATTRIBS,
+ }
+ }
+}
+
+impl Raw {
+ pub fn from_cube(cube: &Cube) -> Raw {
+ Raw {
+ transformation: glam::Mat4::from_scale_rotation_translation(
+ glam::vec3(cube.size, cube.size, cube.size),
+ cube.rotation,
+ cube.position,
+ ),
+ normal: glam::Mat3::from_quat(cube.rotation),
+ _padding: [0.0; 3],
+ }
+ }
+
+ pub fn vertices() -> [Vertex; 36] {
+ [
+ //face 1
+ Vertex {
+ pos: vec3(-0.5, -0.5, -0.5),
+ normal: vec3(0.0, 0.0, -1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, -0.5),
+ normal: vec3(0.0, 0.0, -1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, -0.5),
+ normal: vec3(0.0, 0.0, -1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, -0.5),
+ normal: vec3(0.0, 0.0, -1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, 0.5, -0.5),
+ normal: vec3(0.0, 0.0, -1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, -0.5),
+ normal: vec3(0.0, 0.0, -1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ //face 2
+ Vertex {
+ pos: vec3(-0.5, -0.5, 0.5),
+ normal: vec3(0.0, 0.0, 1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, 0.5),
+ normal: vec3(0.0, 0.0, 1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, 0.5),
+ normal: vec3(0.0, 0.0, 1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, 0.5),
+ normal: vec3(0.0, 0.0, 1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, 0.5, 0.5),
+ normal: vec3(0.0, 0.0, 1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, 0.5),
+ normal: vec3(0.0, 0.0, 1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ //face 3
+ Vertex {
+ pos: vec3(-0.5, 0.5, 0.5),
+ normal: vec3(-1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(0.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, 0.5, -0.5),
+ normal: vec3(-1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(1.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, -0.5),
+ normal: vec3(-1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, -0.5),
+ normal: vec3(-1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, 0.5),
+ normal: vec3(-1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(0.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, 0.5, 0.5),
+ normal: vec3(-1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(0.0, 1.0),
+ },
+ //face 4
+ Vertex {
+ pos: vec3(0.5, 0.5, 0.5),
+ normal: vec3(1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(0.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, -0.5),
+ normal: vec3(1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(1.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, -0.5),
+ normal: vec3(1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, -0.5),
+ normal: vec3(1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, 0.5),
+ normal: vec3(1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(0.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, 0.5),
+ normal: vec3(1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(0.0, 1.0),
+ },
+ //face 5
+ Vertex {
+ pos: vec3(-0.5, -0.5, -0.5),
+ normal: vec3(0.0, -1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, -0.5),
+ normal: vec3(0.0, -1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, 0.5),
+ normal: vec3(0.0, -1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, 0.5),
+ normal: vec3(0.0, -1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, 0.5),
+ normal: vec3(0.0, -1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, -0.5),
+ normal: vec3(0.0, -1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ //face 6
+ Vertex {
+ pos: vec3(-0.5, 0.5, -0.5),
+ normal: vec3(0.0, 1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, -0.5),
+ normal: vec3(0.0, 1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, 0.5),
+ normal: vec3(0.0, 1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, 0.5),
+ normal: vec3(0.0, 1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, 0.5, 0.5),
+ normal: vec3(0.0, 1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, 0.5, -0.5),
+ normal: vec3(0.0, 1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ ]
+ }
+}
diff --git a/examples/custom_shader/src/scene/pipeline/uniforms.rs b/examples/custom_shader/src/scene/pipeline/uniforms.rs
new file mode 100644
index 00000000..1eac8292
--- /dev/null
+++ b/examples/custom_shader/src/scene/pipeline/uniforms.rs
@@ -0,0 +1,23 @@
+use crate::scene::Camera;
+
+use iced::{Color, Rectangle};
+
+#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
+#[repr(C)]
+pub struct Uniforms {
+ camera_proj: glam::Mat4,
+ camera_pos: glam::Vec4,
+ light_color: glam::Vec4,
+}
+
+impl Uniforms {
+ pub fn new(camera: &Camera, bounds: Rectangle, light_color: Color) -> Self {
+ let camera_proj = camera.build_view_proj_matrix(bounds);
+
+ Self {
+ camera_proj,
+ camera_pos: camera.position(),
+ light_color: glam::Vec4::from(light_color.into_linear()),
+ }
+ }
+}
diff --git a/examples/custom_shader/src/scene/pipeline/vertex.rs b/examples/custom_shader/src/scene/pipeline/vertex.rs
new file mode 100644
index 00000000..e64cd926
--- /dev/null
+++ b/examples/custom_shader/src/scene/pipeline/vertex.rs
@@ -0,0 +1,31 @@
+use crate::wgpu;
+
+#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+#[repr(C)]
+pub struct Vertex {
+ pub pos: glam::Vec3,
+ pub normal: glam::Vec3,
+ pub tangent: glam::Vec3,
+ pub uv: glam::Vec2,
+}
+
+impl Vertex {
+ const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
+ //position
+ 0 => Float32x3,
+ //normal
+ 1 => Float32x3,
+ //tangent
+ 2 => Float32x3,
+ //uv
+ 3 => Float32x2,
+ ];
+
+ pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
+ wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
+ step_mode: wgpu::VertexStepMode::Vertex,
+ attributes: &Self::ATTRIBS,
+ }
+ }
+}
diff --git a/examples/custom_shader/src/shaders/cubes.wgsl b/examples/custom_shader/src/shaders/cubes.wgsl
new file mode 100644
index 00000000..cd7f94d8
--- /dev/null
+++ b/examples/custom_shader/src/shaders/cubes.wgsl
@@ -0,0 +1,123 @@
+struct Uniforms {
+ projection: mat4x4<f32>,
+ camera_pos: vec4<f32>,
+ light_color: vec4<f32>,
+}
+
+const LIGHT_POS: vec3<f32> = vec3<f32>(0.0, 3.0, 3.0);
+
+@group(0) @binding(0) var<uniform> uniforms: Uniforms;
+@group(0) @binding(1) var sky_texture: texture_cube<f32>;
+@group(0) @binding(2) var tex_sampler: sampler;
+@group(0) @binding(3) var normal_texture: texture_2d<f32>;
+
+struct Vertex {
+ @location(0) position: vec3<f32>,
+ @location(1) normal: vec3<f32>,
+ @location(2) tangent: vec3<f32>,
+ @location(3) uv: vec2<f32>,
+}
+
+struct Cube {
+ @location(4) matrix_0: vec4<f32>,
+ @location(5) matrix_1: vec4<f32>,
+ @location(6) matrix_2: vec4<f32>,
+ @location(7) matrix_3: vec4<f32>,
+ @location(8) normal_matrix_0: vec3<f32>,
+ @location(9) normal_matrix_1: vec3<f32>,
+ @location(10) normal_matrix_2: vec3<f32>,
+}
+
+struct Output {
+ @builtin(position) clip_pos: vec4<f32>,
+ @location(0) uv: vec2<f32>,
+ @location(1) tangent_pos: vec3<f32>,
+ @location(2) tangent_camera_pos: vec3<f32>,
+ @location(3) tangent_light_pos: vec3<f32>,
+}
+
+@vertex
+fn vs_main(vertex: Vertex, cube: Cube) -> Output {
+ let cube_matrix = mat4x4<f32>(
+ cube.matrix_0,
+ cube.matrix_1,
+ cube.matrix_2,
+ cube.matrix_3,
+ );
+
+ let normal_matrix = mat3x3<f32>(
+ cube.normal_matrix_0,
+ cube.normal_matrix_1,
+ cube.normal_matrix_2,
+ );
+
+ //convert to tangent space to calculate lighting in same coordinate space as normal map sample
+ let tangent = normalize(normal_matrix * vertex.tangent);
+ let normal = normalize(normal_matrix * vertex.normal);
+ let bitangent = cross(tangent, normal);
+
+ //shift everything into tangent space
+ let tbn = transpose(mat3x3<f32>(tangent, bitangent, normal));
+
+ let world_pos = cube_matrix * vec4<f32>(vertex.position, 1.0);
+
+ var out: Output;
+ out.clip_pos = uniforms.projection * world_pos;
+ out.uv = vertex.uv;
+ out.tangent_pos = tbn * world_pos.xyz;
+ out.tangent_camera_pos = tbn * uniforms.camera_pos.xyz;
+ out.tangent_light_pos = tbn * LIGHT_POS;
+
+ return out;
+}
+
+//cube properties
+const CUBE_BASE_COLOR: vec4<f32> = vec4<f32>(0.294118, 0.462745, 0.611765, 0.6);
+const SHINE_DAMPER: f32 = 1.0;
+const REFLECTIVITY: f32 = 0.8;
+const REFRACTION_INDEX: f32 = 1.31;
+
+//fog, for the ~* cinematic effect *~
+const FOG_DENSITY: f32 = 0.15;
+const FOG_GRADIENT: f32 = 8.0;
+const FOG_COLOR: vec4<f32> = vec4<f32>(1.0, 1.0, 1.0, 1.0);
+
+@fragment
+fn fs_main(in: Output) -> @location(0) vec4<f32> {
+ let to_camera = in.tangent_camera_pos - in.tangent_pos;
+
+ //normal sample from texture
+ var normal = textureSample(normal_texture, tex_sampler, in.uv).xyz;
+ normal = normal * 2.0 - 1.0;
+
+ //diffuse
+ let dir_to_light: vec3<f32> = normalize(in.tangent_light_pos - in.tangent_pos);
+ let brightness = max(dot(normal, dir_to_light), 0.0);
+ let diffuse: vec3<f32> = brightness * uniforms.light_color.xyz;
+
+ //specular
+ let dir_to_camera = normalize(to_camera);
+ let light_dir = -dir_to_light;
+ let reflected_light_dir = reflect(light_dir, normal);
+ let specular_factor = max(dot(reflected_light_dir, dir_to_camera), 0.0);
+ let damped_factor = pow(specular_factor, SHINE_DAMPER);
+ let specular: vec3<f32> = damped_factor * uniforms.light_color.xyz * REFLECTIVITY;
+
+ //fog
+ let distance = length(to_camera);
+ let visibility = clamp(exp(-pow((distance * FOG_DENSITY), FOG_GRADIENT)), 0.0, 1.0);
+
+ //reflection
+ let reflection_dir = reflect(dir_to_camera, normal);
+ let reflection_color = textureSample(sky_texture, tex_sampler, reflection_dir);
+ let refraction_dir = refract(dir_to_camera, normal, REFRACTION_INDEX);
+ let refraction_color = textureSample(sky_texture, tex_sampler, refraction_dir);
+ let final_reflect_color = mix(reflection_color, refraction_color, 0.5);
+
+ //mix it all together!
+ var color = vec4<f32>(CUBE_BASE_COLOR.xyz * diffuse + specular, CUBE_BASE_COLOR.w);
+ color = mix(color, final_reflect_color, 0.8);
+ color = mix(FOG_COLOR, color, visibility);
+
+ return color;
+}
diff --git a/examples/custom_shader/src/shaders/depth.wgsl b/examples/custom_shader/src/shaders/depth.wgsl
new file mode 100644
index 00000000..a3f7e5ec
--- /dev/null
+++ b/examples/custom_shader/src/shaders/depth.wgsl
@@ -0,0 +1,48 @@
+var<private> positions: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(1.0, -1.0),
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>(1.0, 1.0),
+ vec2<f32>(1.0, -1.0)
+);
+
+var<private> uvs: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
+ vec2<f32>(0.0, 0.0),
+ vec2<f32>(0.0, 1.0),
+ vec2<f32>(1.0, 1.0),
+ vec2<f32>(0.0, 0.0),
+ vec2<f32>(1.0, 0.0),
+ vec2<f32>(1.0, 1.0)
+);
+
+@group(0) @binding(0) var depth_sampler: sampler;
+@group(0) @binding(1) var depth_texture: texture_2d<f32>;
+
+struct Output {
+ @builtin(position) position: vec4<f32>,
+ @location(0) uv: vec2<f32>,
+}
+
+@vertex
+fn vs_main(@builtin(vertex_index) v_index: u32) -> Output {
+ var out: Output;
+
+ out.position = vec4<f32>(positions[v_index], 0.0, 1.0);
+ out.uv = uvs[v_index];
+
+ return out;
+}
+
+@fragment
+fn fs_main(input: Output) -> @location(0) vec4<f32> {
+ let depth = textureSample(depth_texture, depth_sampler, input.uv).r;
+
+ if (depth > .9999) {
+ discard;
+ }
+
+ let c = 1.0 - depth;
+
+ return vec4<f32>(c, c, c, 1.0);
+}
diff --git a/examples/custom_shader/textures/ice_cube_normal_map.png b/examples/custom_shader/textures/ice_cube_normal_map.png
new file mode 100644
index 00000000..7b4b7228
--- /dev/null
+++ b/examples/custom_shader/textures/ice_cube_normal_map.png
Binary files differ
diff --git a/examples/custom_shader/textures/skybox/neg_x.jpg b/examples/custom_shader/textures/skybox/neg_x.jpg
new file mode 100644
index 00000000..00cc783d
--- /dev/null
+++ b/examples/custom_shader/textures/skybox/neg_x.jpg
Binary files differ
diff --git a/examples/custom_shader/textures/skybox/neg_y.jpg b/examples/custom_shader/textures/skybox/neg_y.jpg
new file mode 100644
index 00000000..548f6445
--- /dev/null
+++ b/examples/custom_shader/textures/skybox/neg_y.jpg
Binary files differ
diff --git a/examples/custom_shader/textures/skybox/neg_z.jpg b/examples/custom_shader/textures/skybox/neg_z.jpg
new file mode 100644
index 00000000..5698512e
--- /dev/null
+++ b/examples/custom_shader/textures/skybox/neg_z.jpg
Binary files differ
diff --git a/examples/custom_shader/textures/skybox/pos_x.jpg b/examples/custom_shader/textures/skybox/pos_x.jpg
new file mode 100644
index 00000000..dddecba7
--- /dev/null
+++ b/examples/custom_shader/textures/skybox/pos_x.jpg
Binary files differ
diff --git a/examples/custom_shader/textures/skybox/pos_y.jpg b/examples/custom_shader/textures/skybox/pos_y.jpg
new file mode 100644
index 00000000..361427fd
--- /dev/null
+++ b/examples/custom_shader/textures/skybox/pos_y.jpg
Binary files differ
diff --git a/examples/custom_shader/textures/skybox/pos_z.jpg b/examples/custom_shader/textures/skybox/pos_z.jpg
new file mode 100644
index 00000000..0085a49e
--- /dev/null
+++ b/examples/custom_shader/textures/skybox/pos_z.jpg
Binary files differ
diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs
index 32a14cbe..7ffb4cd0 100644
--- a/examples/custom_widget/src/main.rs
+++ b/examples/custom_widget/src/main.rs
@@ -33,12 +33,11 @@ mod circle {
where
Renderer: renderer::Renderer,
{
- fn width(&self) -> Length {
- Length::Shrink
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Shrink,
+ height: Length::Shrink,
+ }
}
fn layout(
diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs
index a2fcb275..675e9e26 100644
--- a/examples/download_progress/src/main.rs
+++ b/examples/download_progress/src/main.rs
@@ -73,16 +73,15 @@ impl Application for Example {
}
fn view(&self) -> Element<Message> {
- let downloads = Column::with_children(
- self.downloads.iter().map(Download::view).collect(),
- )
- .push(
- button("Add another download")
- .on_press(Message::Add)
- .padding(10),
- )
- .spacing(20)
- .align_items(Alignment::End);
+ let downloads =
+ Column::with_children(self.downloads.iter().map(Download::view))
+ .push(
+ button("Add another download")
+ .on_press(Message::Add)
+ .padding(10),
+ )
+ .spacing(20)
+ .align_items(Alignment::End);
container(downloads)
.width(Length::Fill)
diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml
new file mode 100644
index 00000000..dc885728
--- /dev/null
+++ b/examples/editor/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "editor"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector@hecrj.dev>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced.workspace = true
+iced.features = ["highlighter", "tokio", "debug"]
+
+tokio.workspace = true
+tokio.features = ["fs"]
+
+rfd = "0.13"
diff --git a/examples/editor/fonts/icons.ttf b/examples/editor/fonts/icons.ttf
new file mode 100644
index 00000000..393c6922
--- /dev/null
+++ b/examples/editor/fonts/icons.ttf
Binary files differ
diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs
new file mode 100644
index 00000000..bf2aaaa3
--- /dev/null
+++ b/examples/editor/src/main.rs
@@ -0,0 +1,312 @@
+use iced::executor;
+use iced::highlighter::{self, Highlighter};
+use iced::keyboard;
+use iced::theme::{self, Theme};
+use iced::widget::{
+ button, column, container, horizontal_space, pick_list, row, text,
+ text_editor, tooltip,
+};
+use iced::{
+ Alignment, Application, Command, Element, Font, Length, Settings,
+ Subscription,
+};
+
+use std::ffi;
+use std::io;
+use std::path::{Path, PathBuf};
+use std::sync::Arc;
+
+pub fn main() -> iced::Result {
+ Editor::run(Settings {
+ fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()],
+ default_font: Font::MONOSPACE,
+ ..Settings::default()
+ })
+}
+
+struct Editor {
+ file: Option<PathBuf>,
+ content: text_editor::Content,
+ theme: highlighter::Theme,
+ is_loading: bool,
+ is_dirty: bool,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ ActionPerformed(text_editor::Action),
+ ThemeSelected(highlighter::Theme),
+ NewFile,
+ OpenFile,
+ FileOpened(Result<(PathBuf, Arc<String>), Error>),
+ SaveFile,
+ FileSaved(Result<PathBuf, Error>),
+}
+
+impl Application for Editor {
+ type Message = Message;
+ type Theme = Theme;
+ type Executor = executor::Default;
+ type Flags = ();
+
+ fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
+ (
+ Self {
+ file: None,
+ content: text_editor::Content::new(),
+ theme: highlighter::Theme::SolarizedDark,
+ is_loading: true,
+ is_dirty: false,
+ },
+ Command::perform(load_file(default_file()), Message::FileOpened),
+ )
+ }
+
+ fn title(&self) -> String {
+ String::from("Editor - Iced")
+ }
+
+ fn update(&mut self, message: Message) -> Command<Message> {
+ match message {
+ Message::ActionPerformed(action) => {
+ self.is_dirty = self.is_dirty || action.is_edit();
+
+ self.content.perform(action);
+
+ Command::none()
+ }
+ Message::ThemeSelected(theme) => {
+ self.theme = theme;
+
+ Command::none()
+ }
+ Message::NewFile => {
+ if !self.is_loading {
+ self.file = None;
+ self.content = text_editor::Content::new();
+ }
+
+ Command::none()
+ }
+ Message::OpenFile => {
+ if self.is_loading {
+ Command::none()
+ } else {
+ self.is_loading = true;
+
+ Command::perform(open_file(), Message::FileOpened)
+ }
+ }
+ Message::FileOpened(result) => {
+ self.is_loading = false;
+ self.is_dirty = false;
+
+ if let Ok((path, contents)) = result {
+ self.file = Some(path);
+ self.content = text_editor::Content::with_text(&contents);
+ }
+
+ Command::none()
+ }
+ Message::SaveFile => {
+ if self.is_loading {
+ Command::none()
+ } else {
+ self.is_loading = true;
+
+ Command::perform(
+ save_file(self.file.clone(), self.content.text()),
+ Message::FileSaved,
+ )
+ }
+ }
+ Message::FileSaved(result) => {
+ self.is_loading = false;
+
+ if let Ok(path) = result {
+ self.file = Some(path);
+ self.is_dirty = false;
+ }
+
+ Command::none()
+ }
+ }
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ keyboard::on_key_press(|key, modifiers| match key.as_ref() {
+ keyboard::Key::Character("s") if modifiers.command() => {
+ Some(Message::SaveFile)
+ }
+ _ => None,
+ })
+ }
+
+ fn view(&self) -> Element<Message> {
+ let controls = row![
+ action(new_icon(), "New file", Some(Message::NewFile)),
+ action(
+ open_icon(),
+ "Open file",
+ (!self.is_loading).then_some(Message::OpenFile)
+ ),
+ action(
+ save_icon(),
+ "Save file",
+ self.is_dirty.then_some(Message::SaveFile)
+ ),
+ horizontal_space(Length::Fill),
+ pick_list(
+ highlighter::Theme::ALL,
+ Some(self.theme),
+ Message::ThemeSelected
+ )
+ .text_size(14)
+ .padding([5, 10])
+ ]
+ .spacing(10)
+ .align_items(Alignment::Center);
+
+ let status = row![
+ text(if let Some(path) = &self.file {
+ let path = path.display().to_string();
+
+ if path.len() > 60 {
+ format!("...{}", &path[path.len() - 40..])
+ } else {
+ path
+ }
+ } else {
+ String::from("New file")
+ }),
+ horizontal_space(Length::Fill),
+ text({
+ let (line, column) = self.content.cursor_position();
+
+ format!("{}:{}", line + 1, column + 1)
+ })
+ ]
+ .spacing(10);
+
+ column![
+ controls,
+ text_editor(&self.content)
+ .on_action(Message::ActionPerformed)
+ .highlight::<Highlighter>(
+ highlighter::Settings {
+ theme: self.theme,
+ extension: self
+ .file
+ .as_deref()
+ .and_then(Path::extension)
+ .and_then(ffi::OsStr::to_str)
+ .map(str::to_string)
+ .unwrap_or(String::from("rs")),
+ },
+ |highlight, _theme| highlight.to_format()
+ ),
+ status,
+ ]
+ .spacing(10)
+ .padding(10)
+ .into()
+ }
+
+ fn theme(&self) -> Theme {
+ if self.theme.is_dark() {
+ Theme::Dark
+ } else {
+ Theme::Light
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum Error {
+ DialogClosed,
+ IoError(io::ErrorKind),
+}
+
+fn default_file() -> PathBuf {
+ PathBuf::from(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR")))
+}
+
+async fn open_file() -> Result<(PathBuf, Arc<String>), Error> {
+ let picked_file = rfd::AsyncFileDialog::new()
+ .set_title("Open a text file...")
+ .pick_file()
+ .await
+ .ok_or(Error::DialogClosed)?;
+
+ load_file(picked_file.path().to_owned()).await
+}
+
+async fn load_file(path: PathBuf) -> Result<(PathBuf, Arc<String>), Error> {
+ let contents = tokio::fs::read_to_string(&path)
+ .await
+ .map(Arc::new)
+ .map_err(|error| Error::IoError(error.kind()))?;
+
+ Ok((path, contents))
+}
+
+async fn save_file(
+ path: Option<PathBuf>,
+ contents: String,
+) -> Result<PathBuf, Error> {
+ let path = if let Some(path) = path {
+ path
+ } else {
+ rfd::AsyncFileDialog::new()
+ .save_file()
+ .await
+ .as_ref()
+ .map(rfd::FileHandle::path)
+ .map(Path::to_owned)
+ .ok_or(Error::DialogClosed)?
+ };
+
+ tokio::fs::write(&path, contents)
+ .await
+ .map_err(|error| Error::IoError(error.kind()))?;
+
+ Ok(path)
+}
+
+fn action<'a, Message: Clone + 'a>(
+ content: impl Into<Element<'a, Message>>,
+ label: &'a str,
+ on_press: Option<Message>,
+) -> Element<'a, Message> {
+ let action = button(container(content).width(30).center_x());
+
+ if let Some(on_press) = on_press {
+ tooltip(
+ action.on_press(on_press),
+ label,
+ tooltip::Position::FollowCursor,
+ )
+ .style(theme::Container::Box)
+ .into()
+ } else {
+ action.style(theme::Button::Secondary).into()
+ }
+}
+
+fn new_icon<'a, Message>() -> Element<'a, Message> {
+ icon('\u{0e800}')
+}
+
+fn save_icon<'a, Message>() -> Element<'a, Message> {
+ icon('\u{0e801}')
+}
+
+fn open_icon<'a, Message>() -> Element<'a, Message> {
+ icon('\u{0f115}')
+}
+
+fn icon<'a, Message>(codepoint: char) -> Element<'a, Message> {
+ const ICON_FONT: Font = Font::with_name("editor-icons");
+
+ text(codepoint).font(ICON_FONT).into()
+}
diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs
index 32d0da2c..fc51ac4a 100644
--- a/examples/events/src/main.rs
+++ b/examples/events/src/main.rs
@@ -10,7 +10,10 @@ use iced::{
pub fn main() -> iced::Result {
Events::run(Settings {
- exit_on_close_request: false,
+ window: window::Settings {
+ exit_on_close_request: false,
+ ..window::Settings::default()
+ },
..Settings::default()
})
}
@@ -54,8 +57,9 @@ impl Application for Events {
Command::none()
}
Message::EventOccurred(event) => {
- if let Event::Window(window::Event::CloseRequested) = event {
- window::close()
+ if let Event::Window(id, window::Event::CloseRequested) = event
+ {
+ window::close(id)
} else {
Command::none()
}
@@ -65,7 +69,7 @@ impl Application for Events {
Command::none()
}
- Message::Exit => window::close(),
+ Message::Exit => window::close(window::Id::MAIN),
}
}
@@ -78,8 +82,7 @@ impl Application for Events {
self.last
.iter()
.map(|event| text(format!("{event:?}")).size(40))
- .map(Element::from)
- .collect(),
+ .map(Element::from),
);
let toggle = checkbox(
diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs
index 6152f627..ec618dc1 100644
--- a/examples/exit/src/main.rs
+++ b/examples/exit/src/main.rs
@@ -34,7 +34,7 @@ impl Application for Exit {
fn update(&mut self, message: Message) -> Command<Message> {
match message {
- Message::Confirm => window::close(),
+ Message::Confirm => window::close(window::Id::MAIN),
Message::Exit => {
self.show_confirm = true;
diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml
index 9b291de8..7596844c 100644
--- a/examples/game_of_life/Cargo.toml
+++ b/examples/game_of_life/Cargo.toml
@@ -9,7 +9,7 @@ publish = false
iced.workspace = true
iced.features = ["debug", "canvas", "tokio"]
-itertools = "0.11"
+itertools = "0.12"
rustc-hash.workspace = true
tokio = { workspace = true, features = ["sync"] }
tracing-subscriber = "0.3"
diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs
index 96840143..56f7afd5 100644
--- a/examples/game_of_life/src/main.rs
+++ b/examples/game_of_life/src/main.rs
@@ -146,7 +146,8 @@ impl Application for GameOfLife {
.view()
.map(move |message| Message::Grid(message, version)),
controls,
- ];
+ ]
+ .height(Length::Fill);
container(content)
.width(Length::Fill)
@@ -178,7 +179,6 @@ fn view_controls<'a>(
slider(1.0..=1000.0, speed as f32, Message::SpeedChanged),
text(format!("x{speed}")).size(16),
]
- .width(Length::Fill)
.align_items(Alignment::Center)
.spacing(10);
diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs
index 8ab3b493..5cf9963d 100644
--- a/examples/geometry/src/main.rs
+++ b/examples/geometry/src/main.rs
@@ -16,12 +16,11 @@ mod rainbow {
}
impl<Message> Widget<Message, Renderer> for Rainbow {
- fn width(&self) -> Length {
- Length::Fill
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Fill,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -30,9 +29,9 @@ mod rainbow {
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let size = limits.width(Length::Fill).resolve(Size::ZERO);
+ let width = limits.max().width;
- layout::Node::new(Size::new(size.width, size.width))
+ layout::Node::new(Size::new(width, width))
}
fn draw(
diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs
index 4714c397..89a595c1 100644
--- a/examples/integration/src/controls.rs
+++ b/examples/integration/src/controls.rs
@@ -81,32 +81,25 @@ impl Program for Controls {
);
Row::new()
- .width(Length::Fill)
.height(Length::Fill)
.align_items(Alignment::End)
.push(
- Column::new()
- .width(Length::Fill)
- .align_items(Alignment::End)
- .push(
- Column::new()
- .padding(10)
- .spacing(10)
- .push(
- Text::new("Background color")
- .style(Color::WHITE),
- )
- .push(sliders)
- .push(
- Text::new(format!("{background_color:?}"))
- .size(14)
- .style(Color::WHITE),
- )
- .push(
- text_input("Placeholder", text)
- .on_input(Message::TextChanged),
- ),
- ),
+ Column::new().align_items(Alignment::End).push(
+ Column::new()
+ .padding(10)
+ .spacing(10)
+ .push(Text::new("Background color").style(Color::WHITE))
+ .push(sliders)
+ .push(
+ Text::new(format!("{background_color:?}"))
+ .size(14)
+ .style(Color::WHITE),
+ )
+ .push(
+ text_input("Placeholder", text)
+ .on_input(Message::TextChanged),
+ ),
+ ),
)
.into()
}
diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs
index c26d52fe..ed61459f 100644
--- a/examples/integration/src/main.rs
+++ b/examples/integration/src/main.rs
@@ -6,19 +6,26 @@ use scene::Scene;
use iced_wgpu::graphics::Viewport;
use iced_wgpu::{wgpu, Backend, Renderer, Settings};
+use iced_winit::conversion;
use iced_winit::core::mouse;
use iced_winit::core::renderer;
+use iced_winit::core::window;
use iced_winit::core::{Color, Font, Pixels, Size};
+use iced_winit::futures;
use iced_winit::runtime::program;
use iced_winit::runtime::Debug;
use iced_winit::style::Theme;
-use iced_winit::{conversion, futures, winit, Clipboard};
+use iced_winit::winit;
+use iced_winit::Clipboard;
use winit::{
- event::{Event, ModifiersState, WindowEvent},
+ event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
+ keyboard::ModifiersState,
};
+use std::sync::Arc;
+
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsCast;
#[cfg(target_arch = "wasm32")]
@@ -44,7 +51,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
// Initialize winit
- let event_loop = EventLoop::new();
+ let event_loop = EventLoop::new()?;
#[cfg(target_arch = "wasm32")]
let window = winit::window::WindowBuilder::new()
@@ -54,6 +61,8 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(not(target_arch = "wasm32"))]
let window = winit::window::Window::new(&event_loop)?;
+ let window = Arc::new(window);
+
let physical_size = window.inner_size();
let mut viewport = Viewport::with_physical_size(
Size::new(physical_size.width, physical_size.height),
@@ -76,7 +85,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
backends: backend,
..Default::default()
});
- let surface = unsafe { instance.create_surface(&window) }?;
+ let surface = instance.create_surface(window.clone())?;
let (format, (device, queue)) =
futures::futures::executor::block_on(async {
@@ -110,9 +119,9 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
.request_device(
&wgpu::DeviceDescriptor {
label: None,
- features: adapter_features
+ required_features: adapter_features
& wgpu::Features::default(),
- limits: needed_limits,
+ required_limits: needed_limits,
},
None,
)
@@ -131,6 +140,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
+ desired_maximum_frame_latency: 2,
},
);
@@ -156,66 +166,15 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
);
// Run event loop
- event_loop.run(move |event, _, control_flow| {
+ event_loop.run(move |event, window_target| {
// You should change this if you want to render continuosly
- *control_flow = ControlFlow::Wait;
+ window_target.set_control_flow(ControlFlow::Wait);
match event {
- Event::WindowEvent { event, .. } => {
- match event {
- WindowEvent::CursorMoved { position, .. } => {
- cursor_position = Some(position);
- }
- WindowEvent::ModifiersChanged(new_modifiers) => {
- modifiers = new_modifiers;
- }
- WindowEvent::Resized(_) => {
- resized = true;
- }
- WindowEvent::CloseRequested => {
- *control_flow = ControlFlow::Exit;
- }
- _ => {}
- }
-
- // Map window event to iced event
- if let Some(event) = iced_winit::conversion::window_event(
- &event,
- window.scale_factor(),
- modifiers,
- ) {
- state.queue_event(event);
- }
- }
- Event::MainEventsCleared => {
- // If there are events pending
- if !state.is_queue_empty() {
- // We update iced
- let _ = state.update(
- viewport.logical_size(),
- cursor_position
- .map(|p| {
- conversion::cursor_position(
- p,
- viewport.scale_factor(),
- )
- })
- .map(mouse::Cursor::Available)
- .unwrap_or(mouse::Cursor::Unavailable),
- &mut renderer,
- &Theme::Dark,
- &renderer::Style {
- text_color: Color::WHITE,
- },
- &mut clipboard,
- &mut debug,
- );
-
- // and request a redraw
- window.request_redraw();
- }
- }
- Event::RedrawRequested(_) => {
+ Event::WindowEvent {
+ event: WindowEvent::RedrawRequested,
+ ..
+ } => {
if resized {
let size = window.inner_size();
@@ -234,6 +193,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
+ desired_maximum_frame_latency: 2,
},
);
@@ -271,6 +231,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
&queue,
&mut encoder,
None,
+ frame.texture.format(),
&view,
primitive,
&viewport,
@@ -303,7 +264,60 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
},
}
}
+ Event::WindowEvent { event, .. } => {
+ match event {
+ WindowEvent::CursorMoved { position, .. } => {
+ cursor_position = Some(position);
+ }
+ WindowEvent::ModifiersChanged(new_modifiers) => {
+ modifiers = new_modifiers.state();
+ }
+ WindowEvent::Resized(_) => {
+ resized = true;
+ }
+ WindowEvent::CloseRequested => {
+ window_target.exit();
+ }
+ _ => {}
+ }
+
+ // Map window event to iced event
+ if let Some(event) = iced_winit::conversion::window_event(
+ window::Id::MAIN,
+ event,
+ window.scale_factor(),
+ modifiers,
+ ) {
+ state.queue_event(event);
+ }
+ }
_ => {}
}
- })
+
+ // If there are events pending
+ if !state.is_queue_empty() {
+ // We update iced
+ let _ = state.update(
+ viewport.logical_size(),
+ cursor_position
+ .map(|p| {
+ conversion::cursor_position(p, viewport.scale_factor())
+ })
+ .map(mouse::Cursor::Available)
+ .unwrap_or(mouse::Cursor::Unavailable),
+ &mut renderer,
+ &Theme::Dark,
+ &renderer::Style {
+ text_color: Color::WHITE,
+ },
+ &mut clipboard,
+ &mut debug,
+ );
+
+ // and request a redraw
+ window.request_redraw();
+ }
+ })?;
+
+ Ok(())
}
diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs
index 01808f40..e29558bf 100644
--- a/examples/integration/src/scene.rs
+++ b/examples/integration/src/scene.rs
@@ -36,10 +36,12 @@ impl Scene {
a: a as f64,
}
}),
- store: true,
+ store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
})
}
diff --git a/examples/layout/Cargo.toml b/examples/layout/Cargo.toml
new file mode 100644
index 00000000..855f98d0
--- /dev/null
+++ b/examples/layout/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "layout"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["canvas"] }
diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs
new file mode 100644
index 00000000..6cf0e570
--- /dev/null
+++ b/examples/layout/src/main.rs
@@ -0,0 +1,371 @@
+use iced::executor;
+use iced::keyboard;
+use iced::mouse;
+use iced::theme;
+use iced::widget::{
+ button, canvas, checkbox, column, container, horizontal_space, pick_list,
+ row, scrollable, text, vertical_rule,
+};
+use iced::{
+ color, Alignment, Application, Color, Command, Element, Font, Length,
+ Point, Rectangle, Renderer, Settings, Subscription, Theme,
+};
+
+pub fn main() -> iced::Result {
+ Layout::run(Settings::default())
+}
+
+#[derive(Debug)]
+struct Layout {
+ example: Example,
+ explain: bool,
+ theme: Theme,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ Next,
+ Previous,
+ ExplainToggled(bool),
+ ThemeSelected(Theme),
+}
+
+impl Application for Layout {
+ type Message = Message;
+ type Theme = Theme;
+ type Executor = executor::Default;
+ type Flags = ();
+
+ fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
+ (
+ Self {
+ example: Example::default(),
+ explain: false,
+ theme: Theme::Light,
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ format!("{} - Layout - Iced", self.example.title)
+ }
+
+ fn update(&mut self, message: Self::Message) -> Command<Message> {
+ match message {
+ Message::Next => {
+ self.example = self.example.next();
+ }
+ Message::Previous => {
+ self.example = self.example.previous();
+ }
+ Message::ExplainToggled(explain) => {
+ self.explain = explain;
+ }
+ Message::ThemeSelected(theme) => {
+ self.theme = theme;
+ }
+ }
+
+ Command::none()
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ use keyboard::key;
+
+ keyboard::on_key_release(|key, _modifiers| match key {
+ keyboard::Key::Named(key::Named::ArrowLeft) => {
+ Some(Message::Previous)
+ }
+ keyboard::Key::Named(key::Named::ArrowRight) => Some(Message::Next),
+ _ => None,
+ })
+ }
+
+ fn view(&self) -> Element<Message> {
+ let header = row![
+ text(self.example.title).size(20).font(Font::MONOSPACE),
+ horizontal_space(Length::Fill),
+ checkbox("Explain", self.explain, Message::ExplainToggled),
+ pick_list(
+ Theme::ALL,
+ Some(self.theme.clone()),
+ Message::ThemeSelected
+ ),
+ ]
+ .spacing(20)
+ .align_items(Alignment::Center);
+
+ let example = container(if self.explain {
+ self.example.view().explain(color!(0x0000ff))
+ } else {
+ self.example.view()
+ })
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+
+ container::Appearance::default()
+ .with_border(palette.background.strong.color, 4.0)
+ })
+ .padding(4)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y();
+
+ let controls = row([
+ (!self.example.is_first()).then_some(
+ button("← Previous")
+ .padding([5, 10])
+ .on_press(Message::Previous)
+ .into(),
+ ),
+ Some(horizontal_space(Length::Fill).into()),
+ (!self.example.is_last()).then_some(
+ button("Next →")
+ .padding([5, 10])
+ .on_press(Message::Next)
+ .into(),
+ ),
+ ]
+ .into_iter()
+ .flatten());
+
+ column![header, example, controls]
+ .spacing(10)
+ .padding(20)
+ .into()
+ }
+
+ fn theme(&self) -> Theme {
+ self.theme.clone()
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+struct Example {
+ title: &'static str,
+ view: fn() -> Element<'static, Message>,
+}
+
+impl Example {
+ const LIST: &'static [Self] = &[
+ Self {
+ title: "Centered",
+ view: centered,
+ },
+ Self {
+ title: "Column",
+ view: column_,
+ },
+ Self {
+ title: "Row",
+ view: row_,
+ },
+ Self {
+ title: "Space",
+ view: space,
+ },
+ Self {
+ title: "Application",
+ view: application,
+ },
+ Self {
+ title: "Nested Quotes",
+ view: nested_quotes,
+ },
+ ];
+
+ fn is_first(self) -> bool {
+ Self::LIST.first() == Some(&self)
+ }
+
+ fn is_last(self) -> bool {
+ Self::LIST.last() == Some(&self)
+ }
+
+ fn previous(self) -> Self {
+ let Some(index) =
+ Self::LIST.iter().position(|&example| example == self)
+ else {
+ return self;
+ };
+
+ Self::LIST
+ .get(index.saturating_sub(1))
+ .copied()
+ .unwrap_or(self)
+ }
+
+ fn next(self) -> Self {
+ let Some(index) =
+ Self::LIST.iter().position(|&example| example == self)
+ else {
+ return self;
+ };
+
+ Self::LIST.get(index + 1).copied().unwrap_or(self)
+ }
+
+ fn view(&self) -> Element<Message> {
+ (self.view)()
+ }
+}
+
+impl Default for Example {
+ fn default() -> Self {
+ Self::LIST[0]
+ }
+}
+
+fn centered<'a>() -> Element<'a, Message> {
+ container(text("I am centered!").size(50))
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+}
+
+fn column_<'a>() -> Element<'a, Message> {
+ column![
+ "A column can be used to",
+ "lay out widgets vertically.",
+ square(50),
+ square(50),
+ square(50),
+ "The amount of space between",
+ "elements can be configured!",
+ ]
+ .spacing(40)
+ .into()
+}
+
+fn row_<'a>() -> Element<'a, Message> {
+ row![
+ "A row works like a column...",
+ square(50),
+ square(50),
+ square(50),
+ "but lays out widgets horizontally!",
+ ]
+ .spacing(40)
+ .into()
+}
+
+fn space<'a>() -> Element<'a, Message> {
+ row!["Left!", horizontal_space(Length::Fill), "Right!"].into()
+}
+
+fn application<'a>() -> Element<'a, Message> {
+ let header = container(
+ row![
+ square(40),
+ horizontal_space(Length::Fill),
+ "Header!",
+ horizontal_space(Length::Fill),
+ square(40),
+ ]
+ .padding(10)
+ .align_items(Alignment::Center),
+ )
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+
+ container::Appearance::default()
+ .with_border(palette.background.strong.color, 1)
+ });
+
+ let sidebar = container(
+ column!["Sidebar!", square(50), square(50)]
+ .spacing(40)
+ .padding(10)
+ .width(200)
+ .align_items(Alignment::Center),
+ )
+ .style(theme::Container::Box)
+ .height(Length::Fill)
+ .center_y();
+
+ let content = container(
+ scrollable(
+ column![
+ "Content!",
+ square(400),
+ square(200),
+ square(400),
+ "The end"
+ ]
+ .spacing(40)
+ .align_items(Alignment::Center)
+ .width(Length::Fill),
+ )
+ .height(Length::Fill),
+ )
+ .padding(10);
+
+ column![header, row![sidebar, content]].into()
+}
+
+fn nested_quotes<'a>() -> Element<'a, Message> {
+ (1..5)
+ .fold(column![text("Original text")].padding(10), |quotes, i| {
+ column![
+ container(
+ row![vertical_rule(2), quotes].height(Length::Shrink)
+ )
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+
+ container::Appearance::default().with_background(
+ if palette.is_dark {
+ Color {
+ a: 0.01,
+ ..Color::WHITE
+ }
+ } else {
+ Color {
+ a: 0.08,
+ ..Color::BLACK
+ }
+ },
+ )
+ }),
+ text(format!("Reply {i}"))
+ ]
+ .spacing(10)
+ .padding(10)
+ })
+ .into()
+}
+
+fn square<'a>(size: impl Into<Length> + Copy) -> Element<'a, Message> {
+ struct Square;
+
+ impl canvas::Program<Message> for Square {
+ type State = ();
+
+ fn draw(
+ &self,
+ _state: &Self::State,
+ renderer: &Renderer,
+ theme: &Theme,
+ bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ ) -> Vec<canvas::Geometry> {
+ let mut frame = canvas::Frame::new(renderer, bounds.size());
+
+ let palette = theme.extended_palette();
+
+ frame.fill_rectangle(
+ Point::ORIGIN,
+ bounds.size(),
+ palette.background.strong.color,
+ );
+
+ vec![frame.into_geometry()]
+ }
+ }
+
+ canvas(Square).width(size).height(size).into()
+}
diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs
index 9bf17c56..04df0744 100644
--- a/examples/lazy/src/main.rs
+++ b/examples/lazy/src/main.rs
@@ -46,7 +46,7 @@ enum Color {
}
impl Color {
- const ALL: &[Color] = &[
+ const ALL: &'static [Color] = &[
Color::Black,
Color::Red,
Color::Orange,
@@ -178,35 +178,23 @@ impl Sandbox for App {
}
});
- column(
- items
- .into_iter()
- .map(|item| {
- let button = button("Delete")
- .on_press(Message::DeleteItem(item.clone()))
- .style(theme::Button::Destructive);
-
- row![
- text(&item.name)
- .style(theme::Text::Color(item.color.into())),
- horizontal_space(Length::Fill),
- pick_list(
- Color::ALL,
- Some(item.color),
- move |color| {
- Message::ItemColorChanged(
- item.clone(),
- color,
- )
- }
- ),
- button
- ]
- .spacing(20)
- .into()
- })
- .collect(),
- )
+ column(items.into_iter().map(|item| {
+ let button = button("Delete")
+ .on_press(Message::DeleteItem(item.clone()))
+ .style(theme::Button::Destructive);
+
+ row![
+ text(&item.name)
+ .style(theme::Text::Color(item.color.into())),
+ horizontal_space(Length::Fill),
+ pick_list(Color::ALL, Some(item.color), move |color| {
+ Message::ItemColorChanged(item.clone(), color)
+ }),
+ button
+ ]
+ .spacing(20)
+ .into()
+ }))
.spacing(10)
});
diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs
index bf01c3b4..2e119979 100644
--- a/examples/loading_spinners/src/circular.rs
+++ b/examples/loading_spinners/src/circular.rs
@@ -244,12 +244,11 @@ where
tree::State::new(State::default())
}
- fn width(&self) -> Length {
- Length::Fixed(self.size)
- }
-
- fn height(&self) -> Length {
- Length::Fixed(self.size)
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Fixed(self.size),
+ height: Length::Fixed(self.size),
+ }
}
fn layout(
@@ -258,10 +257,7 @@ where
_renderer: &iced::Renderer<Theme>,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.size).height(self.size);
- let size = limits.resolve(Size::ZERO);
-
- layout::Node::new(size)
+ layout::atomic(limits, self.size, self.size)
}
fn on_event(
@@ -275,11 +271,9 @@ where
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
- const FRAME_RATE: u64 = 60;
-
let state = tree.state.downcast_mut::<State>();
- if let Event::Window(window::Event::RedrawRequested(now)) = event {
+ if let Event::Window(_, window::Event::RedrawRequested(now)) = event {
state.animation = state.animation.timed_transition(
self.cycle_duration,
self.rotation_duration,
@@ -287,9 +281,7 @@ where
);
state.cache.clear();
- shell.request_redraw(RedrawRequest::At(
- now + Duration::from_millis(1000 / FRAME_RATE),
- ));
+ shell.request_redraw(RedrawRequest::NextFrame);
}
event::Status::Ignored
diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs
index c5bb4791..497e0834 100644
--- a/examples/loading_spinners/src/linear.rs
+++ b/examples/loading_spinners/src/linear.rs
@@ -165,12 +165,11 @@ where
tree::State::new(State::default())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -179,10 +178,7 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
- let size = limits.resolve(Size::ZERO);
-
- layout::Node::new(size)
+ layout::atomic(limits, self.width, self.height)
}
fn on_event(
@@ -196,16 +192,12 @@ where
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
- const FRAME_RATE: u64 = 60;
-
let state = tree.state.downcast_mut::<State>();
- if let Event::Window(window::Event::RedrawRequested(now)) = event {
+ if let Event::Window(_, window::Event::RedrawRequested(now)) = event {
*state = state.timed_transition(self.cycle_duration, now);
- shell.request_redraw(RedrawRequest::At(
- now + Duration::from_millis(1000 / FRAME_RATE),
- ));
+ shell.request_redraw(RedrawRequest::NextFrame);
}
event::Status::Ignored
diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs
index a78e9590..93a4605e 100644
--- a/examples/loading_spinners/src/main.rs
+++ b/examples/loading_spinners/src/main.rs
@@ -96,15 +96,14 @@ impl Application for LoadingSpinners {
container(
column.push(
- row(vec![
- text("Cycle duration:").into(),
+ row![
+ text("Cycle duration:"),
slider(1.0..=1000.0, self.cycle_duration * 100.0, |x| {
Message::CycleDurationChanged(x / 100.0)
})
- .width(200.0)
- .into(),
- text(format!("{:.2}s", self.cycle_duration)).into(),
- ])
+ .width(200.0),
+ text(format!("{:.2}s", self.cycle_duration)),
+ ]
.align_items(iced::Alignment::Center)
.spacing(20.0),
),
diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs
index b0e2c81b..963c839e 100644
--- a/examples/modal/src/main.rs
+++ b/examples/modal/src/main.rs
@@ -1,6 +1,7 @@
use iced::event::{self, Event};
use iced::executor;
use iced::keyboard;
+use iced::keyboard::key;
use iced::theme;
use iced::widget::{
self, button, column, container, horizontal_space, pick_list, row, text,
@@ -85,8 +86,9 @@ impl Application for App {
}
Message::Event(event) => match event {
Event::Keyboard(keyboard::Event::KeyPressed {
- key_code: keyboard::KeyCode::Tab,
+ key: keyboard::Key::Named(key::Named::Tab),
modifiers,
+ ..
}) => {
if modifiers.shift() {
widget::focus_previous()
@@ -95,7 +97,7 @@ impl Application for App {
}
}
Event::Keyboard(keyboard::Event::KeyPressed {
- key_code: keyboard::KeyCode::Escape,
+ key: keyboard::Key::Named(key::Named::Escape),
..
}) => {
self.hide_modal();
@@ -205,7 +207,8 @@ enum Plan {
}
impl Plan {
- pub const ALL: &[Self] = &[Self::Basic, Self::Pro, Self::Enterprise];
+ pub const ALL: &'static [Self] =
+ &[Self::Basic, Self::Pro, Self::Enterprise];
}
impl fmt::Display for Plan {
@@ -230,6 +233,7 @@ mod modal {
use iced::mouse;
use iced::{
BorderRadius, Color, Element, Event, Length, Point, Rectangle, Size,
+ Vector,
};
/// A widget that centers a modal element over some base element
@@ -279,12 +283,8 @@ mod modal {
tree.diff_children(&[&self.base, &self.modal]);
}
- fn width(&self) -> Length {
- self.base.as_widget().width()
- }
-
- fn height(&self) -> Length {
- self.base.as_widget().height()
+ fn size(&self) -> Size<Length> {
+ self.base.as_widget().size()
}
fn layout(
@@ -412,22 +412,20 @@ mod modal {
renderer: &Renderer,
_bounds: Size,
position: Point,
+ _translation: Vector,
) -> layout::Node {
let limits = layout::Limits::new(Size::ZERO, self.size)
.width(Length::Fill)
.height(Length::Fill);
- let mut child = self
+ let child = self
.content
.as_widget()
- .layout(self.tree, renderer, &limits);
-
- child.align(Alignment::Center, Alignment::Center, limits.max());
-
- let mut node = layout::Node::with_children(self.size, vec![child]);
- node.move_to(position);
+ .layout(self.tree, renderer, &limits)
+ .align(Alignment::Center, Alignment::Center, limits.max());
- node
+ layout::Node::with_children(self.size, vec![child])
+ .move_to(position)
}
fn on_event(
diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml
new file mode 100644
index 00000000..2e222dfb
--- /dev/null
+++ b/examples/multi_window/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "multi_window"
+version = "0.1.0"
+authors = ["Bingus <shankern@protonmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["debug", "multi-window"] }
diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs
new file mode 100644
index 00000000..5a5e70c1
--- /dev/null
+++ b/examples/multi_window/src/main.rs
@@ -0,0 +1,215 @@
+use iced::event;
+use iced::executor;
+use iced::multi_window::{self, Application};
+use iced::widget::{button, column, container, scrollable, text, text_input};
+use iced::window;
+use iced::{
+ Alignment, Command, Element, Length, Point, Settings, Subscription, Theme,
+ Vector,
+};
+
+use std::collections::HashMap;
+
+fn main() -> iced::Result {
+ Example::run(Settings::default())
+}
+
+#[derive(Default)]
+struct Example {
+ windows: HashMap<window::Id, Window>,
+ next_window_pos: window::Position,
+}
+
+#[derive(Debug)]
+struct Window {
+ title: String,
+ scale_input: String,
+ current_scale: f64,
+ theme: Theme,
+ input_id: iced::widget::text_input::Id,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ ScaleInputChanged(window::Id, String),
+ ScaleChanged(window::Id, String),
+ TitleChanged(window::Id, String),
+ CloseWindow(window::Id),
+ WindowOpened(window::Id, Option<Point>),
+ WindowClosed(window::Id),
+ NewWindow,
+}
+
+impl multi_window::Application for Example {
+ type Executor = executor::Default;
+ type Message = Message;
+ type Theme = Theme;
+ type Flags = ();
+
+ fn new(_flags: ()) -> (Self, Command<Message>) {
+ (
+ Example {
+ windows: HashMap::from([(window::Id::MAIN, Window::new(1))]),
+ next_window_pos: window::Position::Default,
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self, window: window::Id) -> String {
+ self.windows
+ .get(&window)
+ .map(|window| window.title.clone())
+ .unwrap_or("Example".to_string())
+ }
+
+ fn update(&mut self, message: Message) -> Command<Message> {
+ match message {
+ Message::ScaleInputChanged(id, scale) => {
+ let window =
+ self.windows.get_mut(&id).expect("Window not found!");
+ window.scale_input = scale;
+
+ Command::none()
+ }
+ Message::ScaleChanged(id, scale) => {
+ let window =
+ self.windows.get_mut(&id).expect("Window not found!");
+
+ window.current_scale = scale
+ .parse::<f64>()
+ .unwrap_or(window.current_scale)
+ .clamp(0.5, 5.0);
+
+ Command::none()
+ }
+ Message::TitleChanged(id, title) => {
+ let window =
+ self.windows.get_mut(&id).expect("Window not found.");
+
+ window.title = title;
+
+ Command::none()
+ }
+ Message::CloseWindow(id) => window::close(id),
+ Message::WindowClosed(id) => {
+ self.windows.remove(&id);
+ Command::none()
+ }
+ Message::WindowOpened(id, position) => {
+ if let Some(position) = position {
+ self.next_window_pos = window::Position::Specific(
+ position + Vector::new(20.0, 20.0),
+ );
+ }
+
+ if let Some(window) = self.windows.get(&id) {
+ text_input::focus(window.input_id.clone())
+ } else {
+ Command::none()
+ }
+ }
+ Message::NewWindow => {
+ let count = self.windows.len() + 1;
+
+ let (id, spawn_window) = window::spawn(window::Settings {
+ position: self.next_window_pos,
+ exit_on_close_request: count % 2 == 0,
+ ..Default::default()
+ });
+
+ self.windows.insert(id, Window::new(count));
+
+ spawn_window
+ }
+ }
+ }
+
+ fn view(&self, window: window::Id) -> Element<Message> {
+ let content = self.windows.get(&window).unwrap().view(window);
+
+ container(content)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+ }
+
+ fn theme(&self, window: window::Id) -> Self::Theme {
+ self.windows.get(&window).unwrap().theme.clone()
+ }
+
+ fn scale_factor(&self, window: window::Id) -> f64 {
+ self.windows
+ .get(&window)
+ .map(|window| window.current_scale)
+ .unwrap_or(1.0)
+ }
+
+ fn subscription(&self) -> Subscription<Self::Message> {
+ event::listen_with(|event, _| {
+ if let iced::Event::Window(id, window_event) = event {
+ match window_event {
+ window::Event::CloseRequested => {
+ Some(Message::CloseWindow(id))
+ }
+ window::Event::Opened { position, .. } => {
+ Some(Message::WindowOpened(id, position))
+ }
+ window::Event::Closed => Some(Message::WindowClosed(id)),
+ _ => None,
+ }
+ } else {
+ None
+ }
+ })
+ }
+}
+
+impl Window {
+ fn new(count: usize) -> Self {
+ Self {
+ title: format!("Window_{}", count),
+ scale_input: "1.0".to_string(),
+ current_scale: 1.0,
+ theme: if count % 2 == 0 {
+ Theme::Light
+ } else {
+ Theme::Dark
+ },
+ input_id: text_input::Id::unique(),
+ }
+ }
+
+ fn view(&self, id: window::Id) -> Element<Message> {
+ let scale_input = column![
+ text("Window scale factor:"),
+ text_input("Window Scale", &self.scale_input)
+ .on_input(move |msg| { Message::ScaleInputChanged(id, msg) })
+ .on_submit(Message::ScaleChanged(
+ id,
+ self.scale_input.to_string()
+ ))
+ ];
+
+ let title_input = column![
+ text("Window title:"),
+ text_input("Window Title", &self.title)
+ .on_input(move |msg| { Message::TitleChanged(id, msg) })
+ .id(self.input_id.clone())
+ ];
+
+ let new_window_button =
+ button(text("New Window")).on_press(Message::NewWindow);
+
+ let content = scrollable(
+ column![scale_input, title_input, new_window_button]
+ .spacing(50)
+ .width(Length::Fill)
+ .align_items(Alignment::Center),
+ );
+
+ container(content).width(200).center_x().into()
+ }
+}
diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs
index aa3149bb..d5e5bcbe 100644
--- a/examples/pane_grid/src/main.rs
+++ b/examples/pane_grid/src/main.rs
@@ -220,23 +220,26 @@ const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb(
0x47 as f32 / 255.0,
);
-fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> {
- use keyboard::KeyCode;
+fn handle_hotkey(key: keyboard::Key) -> Option<Message> {
+ use keyboard::key::{self, Key};
use pane_grid::{Axis, Direction};
- let direction = match key_code {
- KeyCode::Up => Some(Direction::Up),
- KeyCode::Down => Some(Direction::Down),
- KeyCode::Left => Some(Direction::Left),
- KeyCode::Right => Some(Direction::Right),
- _ => None,
- };
+ match key.as_ref() {
+ Key::Character("v") => Some(Message::SplitFocused(Axis::Vertical)),
+ Key::Character("h") => Some(Message::SplitFocused(Axis::Horizontal)),
+ Key::Character("w") => Some(Message::CloseFocused),
+ Key::Named(key) => {
+ let direction = match key {
+ key::Named::ArrowUp => Some(Direction::Up),
+ key::Named::ArrowDown => Some(Direction::Down),
+ key::Named::ArrowLeft => Some(Direction::Left),
+ key::Named::ArrowRight => Some(Direction::Right),
+ _ => None,
+ };
- match key_code {
- KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)),
- KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)),
- KeyCode::W => Some(Message::CloseFocused),
- _ => direction.map(Message::FocusAdjacent),
+ direction.map(Message::FocusAdjacent)
+ }
+ _ => None,
}
}
@@ -297,7 +300,6 @@ fn view_content<'a>(
text(format!("{}x{}", size.width, size.height)).size(24),
controls,
]
- .width(Length::Fill)
.spacing(10)
.align_items(Alignment::Center);
diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs
index 21200621..e4d96dc8 100644
--- a/examples/pick_list/src/main.rs
+++ b/examples/pick_list/src/main.rs
@@ -1,4 +1,4 @@
-use iced::widget::{column, container, pick_list, scrollable, vertical_space};
+use iced::widget::{column, pick_list, scrollable, vertical_space};
use iced::{Alignment, Element, Length, Sandbox, Settings};
pub fn main() -> iced::Result {
@@ -52,12 +52,7 @@ impl Sandbox for Example {
.align_items(Alignment::Center)
.spacing(10);
- container(scrollable(content))
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ scrollable(content).into()
}
}
diff --git a/examples/progress_bar/README.md b/examples/progress_bar/README.md
index 1268ac6b..a87829c6 100644
--- a/examples/progress_bar/README.md
+++ b/examples/progress_bar/README.md
@@ -5,7 +5,7 @@ A simple progress bar that can be filled by using a slider.
The __[`main`]__ file contains all the code of the example.
<div align="center">
- <img src="https://iced.rs/examples/pokedex.gif">
+ <img src="https://iced.rs/examples/progress_bar.gif">
</div>
You can run it with `cargo run`:
diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs
index ab0a2ae3..6955551e 100644
--- a/examples/screenshot/src/main.rs
+++ b/examples/screenshot/src/main.rs
@@ -1,11 +1,13 @@
use iced::alignment;
-use iced::keyboard::KeyCode;
-use iced::theme::{Button, Container};
+use iced::executor;
+use iced::keyboard;
+use iced::theme;
use iced::widget::{button, column, container, image, row, text, text_input};
+use iced::window;
use iced::window::screenshot::{self, Screenshot};
use iced::{
- event, executor, keyboard, Alignment, Application, Command, ContentFit,
- Element, Event, Length, Rectangle, Renderer, Subscription, Theme,
+ Alignment, Application, Command, ContentFit, Element, Length, Rectangle,
+ Renderer, Subscription, Theme,
};
use ::image as img;
@@ -70,7 +72,10 @@ impl Application for Example {
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message {
Message::Screenshot => {
- return iced::window::screenshot(Message::ScreenshotData);
+ return iced::window::screenshot(
+ window::Id::MAIN,
+ Message::ScreenshotData,
+ );
}
Message::ScreenshotData(screenshot) => {
self.screenshot = Some(screenshot);
@@ -144,7 +149,7 @@ impl Application for Example {
let image = container(image)
.padding(10)
- .style(Container::Box)
+ .style(theme::Container::Box)
.width(Length::FillPortion(2))
.height(Length::Fill)
.center_x()
@@ -199,9 +204,10 @@ impl Application for Example {
self.screenshot.is_some().then(|| Message::Png),
)
} else {
- button(centered_text("Saving...")).style(Button::Secondary)
+ button(centered_text("Saving..."))
+ .style(theme::Button::Secondary)
}
- .style(Button::Secondary)
+ .style(theme::Button::Secondary)
.padding([10, 20, 10, 20])
.width(Length::Fill)
]
@@ -210,7 +216,7 @@ impl Application for Example {
crop_controls,
button(centered_text("Crop"))
.on_press(Message::Crop)
- .style(Button::Destructive)
+ .style(theme::Button::Destructive)
.padding([10, 20, 10, 20])
.width(Length::Fill),
]
@@ -253,16 +259,10 @@ impl Application for Example {
}
fn subscription(&self) -> Subscription<Self::Message> {
- event::listen_with(|event, status| {
- if let event::Status::Captured = status {
- return None;
- }
+ use keyboard::key;
- if let Event::Keyboard(keyboard::Event::KeyPressed {
- key_code: KeyCode::F5,
- ..
- }) = event
- {
+ keyboard::on_key_press(|key, _modifiers| {
+ if let keyboard::Key::Named(key::Named::F5) = key {
Some(Message::Screenshot)
} else {
None
@@ -298,10 +298,7 @@ fn numeric_input(
) -> Element<'_, Option<u32>> {
text_input(
placeholder,
- &value
- .as_ref()
- .map(ToString::to_string)
- .unwrap_or_else(String::new),
+ &value.as_ref().map(ToString::to_string).unwrap_or_default(),
)
.on_input(move |text| {
if text.is_empty() {
diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs
index d82ea841..4b57a5a4 100644
--- a/examples/scrollable/src/main.rs
+++ b/examples/scrollable/src/main.rs
@@ -147,63 +147,54 @@ impl Application for ScrollableDemo {
text("Scroller width:"),
scroller_width_slider,
]
- .spacing(10)
- .width(Length::Fill);
+ .spacing(10);
- let scroll_orientation_controls = column(vec![
- text("Scrollbar direction:").into(),
+ let scroll_orientation_controls = column![
+ text("Scrollbar direction:"),
radio(
"Vertical",
Direction::Vertical,
Some(self.scrollable_direction),
Message::SwitchDirection,
- )
- .into(),
+ ),
radio(
"Horizontal",
Direction::Horizontal,
Some(self.scrollable_direction),
Message::SwitchDirection,
- )
- .into(),
+ ),
radio(
"Both!",
Direction::Multi,
Some(self.scrollable_direction),
Message::SwitchDirection,
- )
- .into(),
- ])
- .spacing(10)
- .width(Length::Fill);
+ ),
+ ]
+ .spacing(10);
- let scroll_alignment_controls = column(vec![
- text("Scrollable alignment:").into(),
+ let scroll_alignment_controls = column![
+ text("Scrollable alignment:"),
radio(
"Start",
scrollable::Alignment::Start,
Some(self.alignment),
Message::AlignmentChanged,
- )
- .into(),
+ ),
radio(
"End",
scrollable::Alignment::End,
Some(self.alignment),
Message::AlignmentChanged,
)
- .into(),
- ])
- .spacing(10)
- .width(Length::Fill);
+ ]
+ .spacing(10);
let scroll_controls = row![
scroll_slider_controls,
scroll_orientation_controls,
scroll_alignment_controls
]
- .spacing(20)
- .width(Length::Fill);
+ .spacing(20);
let scroll_to_end_button = || {
button("Scroll to end")
@@ -229,11 +220,11 @@ impl Application for ScrollableDemo {
text("End!"),
scroll_to_beginning_button(),
]
- .width(Length::Fill)
.align_items(Alignment::Center)
.padding([40, 0, 40, 0])
.spacing(40),
)
+ .width(Length::Fill)
.height(Length::Fill)
.direction(scrollable::Direction::Vertical(
Properties::new()
@@ -259,6 +250,7 @@ impl Application for ScrollableDemo {
.padding([0, 40, 0, 40])
.spacing(40),
)
+ .width(Length::Fill)
.height(Length::Fill)
.direction(scrollable::Direction::Horizontal(
Properties::new()
@@ -301,6 +293,7 @@ impl Application for ScrollableDemo {
.padding([0, 40, 0, 40])
.spacing(40),
)
+ .width(Length::Fill)
.height(Length::Fill)
.direction({
let properties = Properties::new()
@@ -341,20 +334,11 @@ impl Application for ScrollableDemo {
let content: Element<Message> =
column![scroll_controls, scrollable_content, progress_bars]
- .width(Length::Fill)
- .height(Length::Fill)
.align_items(Alignment::Center)
.spacing(10)
.into();
- Element::from(
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .padding(40)
- .center_x()
- .center_y(),
- )
+ container(content).padding(20).center_x().center_y().into()
}
fn theme(&self) -> Self::Theme {
diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs
index ef935c33..01a114bb 100644
--- a/examples/sierpinski_triangle/src/main.rs
+++ b/examples/sierpinski_triangle/src/main.rs
@@ -79,12 +79,10 @@ impl Application for SierpinskiEmulator {
row![
text(format!("Iteration: {:?}", self.graph.iteration)),
slider(0..=10000, self.graph.iteration, Message::IterationSet)
- .width(Length::Fill)
]
.padding(10)
.spacing(20),
]
- .width(Length::Fill)
.align_items(iced::Alignment::Center)
.into()
}
diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs
index 8295dded..82421a86 100644
--- a/examples/solar_system/src/main.rs
+++ b/examples/solar_system/src/main.rs
@@ -114,14 +114,14 @@ impl State {
pub fn new() -> State {
let now = Instant::now();
- let (width, height) = window::Settings::default().size;
+ let size = window::Settings::default().size;
State {
space_cache: canvas::Cache::default(),
system_cache: canvas::Cache::default(),
start: now,
now,
- stars: Self::generate_stars(width, height),
+ stars: Self::generate_stars(size.width, size.height),
}
}
@@ -130,7 +130,7 @@ impl State {
self.system_cache.clear();
}
- fn generate_stars(width: u32, height: u32) -> Vec<(Point, f32)> {
+ fn generate_stars(width: f32, height: f32) -> Vec<(Point, f32)> {
use rand::Rng;
let mut rng = rand::thread_rng();
@@ -139,12 +139,8 @@ impl State {
.map(|_| {
(
Point::new(
- rng.gen_range(
- (-(width as f32) / 2.0)..(width as f32 / 2.0),
- ),
- rng.gen_range(
- (-(height as f32) / 2.0)..(height as f32 / 2.0),
- ),
+ rng.gen_range((-width / 2.0)..(width / 2.0)),
+ rng.gen_range((-height / 2.0)..(height / 2.0)),
),
rng.gen_range(0.5..1.0),
)
diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs
index 0b0f0607..8a0674c1 100644
--- a/examples/stopwatch/src/main.rs
+++ b/examples/stopwatch/src/main.rs
@@ -86,12 +86,16 @@ impl Application for Stopwatch {
};
fn handle_hotkey(
- key_code: keyboard::KeyCode,
+ key: keyboard::Key,
_modifiers: keyboard::Modifiers,
) -> Option<Message> {
- match key_code {
- keyboard::KeyCode::Space => Some(Message::Toggle),
- keyboard::KeyCode::R => Some(Message::Reset),
+ use keyboard::key;
+
+ match key.as_ref() {
+ keyboard::Key::Named(key::Named::Space) => {
+ Some(Message::Toggle)
+ }
+ keyboard::Key::Character("r") => Some(Message::Reset),
_ => None,
}
}
diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs
index 51538ec2..10f3c79d 100644
--- a/examples/styling/src/main.rs
+++ b/examples/styling/src/main.rs
@@ -53,13 +53,16 @@ impl Sandbox for Styling {
self.theme = match theme {
ThemeType::Light => Theme::Light,
ThemeType::Dark => Theme::Dark,
- ThemeType::Custom => Theme::custom(theme::Palette {
- background: Color::from_rgb(1.0, 0.9, 1.0),
- text: Color::BLACK,
- primary: Color::from_rgb(0.5, 0.5, 0.0),
- success: Color::from_rgb(0.0, 1.0, 0.0),
- danger: Color::from_rgb(1.0, 0.0, 0.0),
- }),
+ ThemeType::Custom => Theme::custom(
+ String::from("Custom"),
+ theme::Palette {
+ background: Color::from_rgb(1.0, 0.9, 1.0),
+ text: Color::BLACK,
+ primary: Color::from_rgb(0.5, 0.5, 0.0),
+ success: Color::from_rgb(0.0, 1.0, 0.0),
+ danger: Color::from_rgb(1.0, 0.0, 0.0),
+ },
+ ),
}
}
Message::InputChanged(value) => self.input_value = value,
@@ -104,10 +107,11 @@ impl Sandbox for Styling {
let progress_bar = progress_bar(0.0..=100.0, self.slider_value);
- let scrollable = scrollable(
- column!["Scroll me!", vertical_space(800), "You did it!"]
- .width(Length::Fill),
- )
+ let scrollable = scrollable(column![
+ "Scroll me!",
+ vertical_space(800),
+ "You did it!"
+ ])
.width(Length::Fill)
.height(100);
diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs
index 4dc92416..3bf4960f 100644
--- a/examples/svg/src/main.rs
+++ b/examples/svg/src/main.rs
@@ -63,7 +63,6 @@ impl Sandbox for Tiger {
container(apply_color_filter).width(Length::Fill).center_x()
]
.spacing(20)
- .width(Length::Fill)
.height(Length::Fill),
)
.width(Length::Fill)
diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs
index 20c3dd42..2e837fa3 100644
--- a/examples/toast/src/main.rs
+++ b/examples/toast/src/main.rs
@@ -1,6 +1,7 @@
use iced::event::{self, Event};
use iced::executor;
use iced::keyboard;
+use iced::keyboard::key;
use iced::widget::{
self, button, column, container, pick_list, row, slider, text, text_input,
};
@@ -93,11 +94,12 @@ impl Application for App {
Command::none()
}
Message::Event(Event::Keyboard(keyboard::Event::KeyPressed {
- key_code: keyboard::KeyCode::Tab,
+ key: keyboard::Key::Named(key::Named::Tab),
modifiers,
+ ..
})) if modifiers.shift() => widget::focus_previous(),
Message::Event(Event::Keyboard(keyboard::Event::KeyPressed {
- key_code: keyboard::KeyCode::Tab,
+ key: keyboard::Key::Named(key::Named::Tab),
..
})) => widget::focus_next(),
Message::Event(_) => Command::none(),
@@ -106,9 +108,7 @@ impl Application for App {
fn view<'a>(&'a self) -> Element<'a, Message> {
let subtitle = |title, content: Element<'a, Message>| {
- column![text(title).size(14), content]
- .width(Length::Fill)
- .spacing(5)
+ column![text(title).size(14), content].spacing(5)
};
let mut add_toast = button("Add Toast");
@@ -153,14 +153,11 @@ impl Application for App {
Message::Timeout
)
.step(1.0)
- .width(Length::Fill)
]
.spacing(5)
.into()
),
- column![add_toast]
- .width(Length::Fill)
- .align_items(Alignment::End)
+ column![add_toast].align_items(Alignment::End)
]
.spacing(10)
.max_width(200),
@@ -210,7 +207,7 @@ mod toast {
}
impl Status {
- pub const ALL: &[Self] =
+ pub const ALL: &'static [Self] =
&[Self::Primary, Self::Secondary, Self::Success, Self::Danger];
}
@@ -318,12 +315,8 @@ mod toast {
}
impl<'a, Message> Widget<Message, Renderer> for Manager<'a, Message> {
- fn width(&self) -> Length {
- self.content.as_widget().width()
- }
-
- fn height(&self) -> Length {
- self.content.as_widget().height()
+ fn size(&self) -> Size<Length> {
+ self.content.as_widget().size()
}
fn layout(
@@ -511,15 +504,16 @@ mod toast {
renderer: &Renderer,
bounds: Size,
position: Point,
+ _translation: Vector,
) -> layout::Node {
- let limits = layout::Limits::new(Size::ZERO, bounds)
- .width(Length::Fill)
- .height(Length::Fill);
+ let limits = layout::Limits::new(Size::ZERO, bounds);
layout::flex::resolve(
layout::flex::Axis::Vertical,
renderer,
&limits,
+ Length::Fill,
+ Length::Fill,
10.into(),
10.0,
Alignment::End,
@@ -538,7 +532,9 @@ mod toast {
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
- if let Event::Window(window::Event::RedrawRequested(now)) = &event {
+ if let Event::Window(_, window::Event::RedrawRequested(now)) =
+ &event
+ {
let mut next_redraw: Option<window::RedrawRequest> = None;
self.instants.iter_mut().enumerate().for_each(
diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs
index 1ad3aba7..3d79f087 100644
--- a/examples/todos/src/main.rs
+++ b/examples/todos/src/main.rs
@@ -8,7 +8,7 @@ use iced::widget::{
};
use iced::window;
use iced::{Application, Element};
-use iced::{Color, Command, Length, Settings, Subscription};
+use iced::{Color, Command, Length, Settings, Size, Subscription};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
@@ -22,7 +22,7 @@ pub fn main() -> iced::Result {
Todos::run(Settings {
window: window::Settings {
- size: (500, 800),
+ size: Size::new(500.0, 800.0),
..window::Settings::default()
},
..Settings::default()
@@ -54,7 +54,7 @@ enum Message {
FilterChanged(Filter),
TaskMessage(usize, TaskMessage),
TabPressed { shift: bool },
- ChangeWindowMode(window::Mode),
+ ToggleFullscreen(window::Mode),
}
impl Application for Todos {
@@ -165,8 +165,8 @@ impl Application for Todos {
widget::focus_next()
}
}
- Message::ChangeWindowMode(mode) => {
- window::change_mode(mode)
+ Message::ToggleFullscreen(mode) => {
+ window::change_mode(window::Id::MAIN, mode)
}
_ => Command::none(),
};
@@ -254,28 +254,28 @@ impl Application for Todos {
.spacing(20)
.max_width(800);
- scrollable(
- container(content)
- .width(Length::Fill)
- .padding(40)
- .center_x(),
- )
- .into()
+ scrollable(container(content).padding(40).center_x()).into()
}
}
}
fn subscription(&self) -> Subscription<Message> {
- keyboard::on_key_press(|key_code, modifiers| {
- match (key_code, modifiers) {
- (keyboard::KeyCode::Tab, _) => Some(Message::TabPressed {
+ use keyboard::key;
+
+ keyboard::on_key_press(|key, modifiers| {
+ let keyboard::Key::Named(key) = key else {
+ return None;
+ };
+
+ match (key, modifiers) {
+ (key::Named::Tab, _) => Some(Message::TabPressed {
shift: modifiers.shift(),
}),
- (keyboard::KeyCode::Up, keyboard::Modifiers::SHIFT) => {
- Some(Message::ChangeWindowMode(window::Mode::Fullscreen))
+ (key::Named::ArrowUp, keyboard::Modifiers::SHIFT) => {
+ Some(Message::ToggleFullscreen(window::Mode::Fullscreen))
}
- (keyboard::KeyCode::Down, keyboard::Modifiers::SHIFT) => {
- Some(Message::ChangeWindowMode(window::Mode::Windowed))
+ (key::Named::ArrowDown, keyboard::Modifiers::SHIFT) => {
+ Some(Message::ToggleFullscreen(window::Mode::Windowed))
}
_ => None,
}
@@ -472,7 +472,6 @@ fn empty_message(message: &str) -> Element<'_, Message> {
.horizontal_alignment(alignment::Horizontal::Center)
.style(Color::from([0.7, 0.7, 0.7])),
)
- .width(Length::Fill)
.height(200)
.center_y()
.into()
diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs
index d46e40d1..8633bc0a 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -1,4 +1,4 @@
-use iced::alignment;
+use iced::alignment::{self, Alignment};
use iced::theme;
use iced::widget::{
checkbox, column, container, horizontal_space, image, radio, row,
@@ -126,7 +126,10 @@ impl Steps {
Step::Toggler {
can_continue: false,
},
- Step::Image { width: 300 },
+ Step::Image {
+ width: 300,
+ filter_method: image::FilterMethod::Linear,
+ },
Step::Scrollable,
Step::TextInput {
value: String::new(),
@@ -195,6 +198,7 @@ enum Step {
},
Image {
width: u16,
+ filter_method: image::FilterMethod,
},
Scrollable,
TextInput {
@@ -215,6 +219,7 @@ pub enum StepMessage {
TextColorChanged(Color),
LanguageSelected(Language),
ImageWidthChanged(u16),
+ ImageUseNearestToggled(bool),
InputChanged(String),
ToggleSecureInput(bool),
ToggleTextInputIcon(bool),
@@ -265,6 +270,15 @@ impl<'a> Step {
*width = new_width;
}
}
+ StepMessage::ImageUseNearestToggled(use_nearest) => {
+ if let Step::Image { filter_method, .. } = self {
+ *filter_method = if use_nearest {
+ image::FilterMethod::Nearest
+ } else {
+ image::FilterMethod::Linear
+ };
+ }
+ }
StepMessage::InputChanged(new_value) => {
if let Step::TextInput { value, .. } = self {
*value = new_value;
@@ -330,7 +344,10 @@ impl<'a> Step {
Step::Toggler { can_continue } => Self::toggler(*can_continue),
Step::Slider { value } => Self::slider(*value),
Step::Text { size, color } => Self::text(*size, *color),
- Step::Image { width } => Self::image(*width),
+ Step::Image {
+ width,
+ filter_method,
+ } => Self::image(*width, *filter_method),
Step::RowsAndColumns { layout, spacing } => {
Self::rows_and_columns(*layout, *spacing)
}
@@ -492,7 +509,6 @@ impl<'a> Step {
)
})
.map(Element::from)
- .collect()
)
.spacing(10)
]
@@ -525,16 +541,25 @@ impl<'a> Step {
)
}
- fn image(width: u16) -> Column<'a, StepMessage> {
+ fn image(
+ width: u16,
+ filter_method: image::FilterMethod,
+ ) -> Column<'a, StepMessage> {
Self::container("Image")
.push("An image that tries to keep its aspect ratio.")
- .push(ferris(width))
+ .push(ferris(width, filter_method))
.push(slider(100..=500, width, StepMessage::ImageWidthChanged))
.push(
text(format!("Width: {width} px"))
.width(Length::Fill)
.horizontal_alignment(alignment::Horizontal::Center),
)
+ .push(checkbox(
+ "Use nearest interpolation",
+ filter_method == image::FilterMethod::Nearest,
+ StepMessage::ImageUseNearestToggled,
+ ))
+ .align_items(Alignment::Center)
}
fn scrollable() -> Column<'a, StepMessage> {
@@ -555,7 +580,7 @@ impl<'a> Step {
.horizontal_alignment(alignment::Horizontal::Center),
)
.push(vertical_space(4096))
- .push(ferris(300))
+ .push(ferris(300, image::FilterMethod::Linear))
.push(
text("You made it!")
.width(Length::Fill)
@@ -646,7 +671,10 @@ impl<'a> Step {
}
}
-fn ferris<'a>(width: u16) -> Container<'a, StepMessage> {
+fn ferris<'a>(
+ width: u16,
+ filter_method: image::FilterMethod,
+) -> Container<'a, StepMessage> {
container(
// This should go away once we unify resource loading on native
// platforms
@@ -655,6 +683,7 @@ fn ferris<'a>(width: u16) -> Container<'a, StepMessage> {
} else {
image(format!("{}/images/ferris.png", env!("CARGO_MANIFEST_DIR")))
}
+ .filter_method(filter_method)
.width(width),
)
.width(Length::Fill)
@@ -662,11 +691,7 @@ fn ferris<'a>(width: u16) -> Container<'a, StepMessage> {
}
fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> {
- iced::widget::button(
- text(label).horizontal_alignment(alignment::Horizontal::Center),
- )
- .padding(12)
- .width(100)
+ iced::widget::button(text(label)).padding([12, 24])
}
fn color_slider<'a>(
diff --git a/examples/vectorial_text/Cargo.toml b/examples/vectorial_text/Cargo.toml
new file mode 100644
index 00000000..76c1af7c
--- /dev/null
+++ b/examples/vectorial_text/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "vectorial_text"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["canvas", "debug"] }
diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs
new file mode 100644
index 00000000..d366b907
--- /dev/null
+++ b/examples/vectorial_text/src/main.rs
@@ -0,0 +1,175 @@
+use iced::alignment::{self, Alignment};
+use iced::mouse;
+use iced::widget::{
+ canvas, checkbox, column, horizontal_space, row, slider, text,
+};
+use iced::{
+ Element, Length, Point, Rectangle, Renderer, Sandbox, Settings, Theme,
+ Vector,
+};
+
+pub fn main() -> iced::Result {
+ VectorialText::run(Settings {
+ antialiasing: true,
+ ..Settings::default()
+ })
+}
+
+struct VectorialText {
+ state: State,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ SizeChanged(f32),
+ AngleChanged(f32),
+ ScaleChanged(f32),
+ ToggleJapanese(bool),
+}
+
+impl Sandbox for VectorialText {
+ type Message = Message;
+
+ fn new() -> Self {
+ Self {
+ state: State::new(),
+ }
+ }
+
+ fn title(&self) -> String {
+ String::from("Vectorial Text - Iced")
+ }
+
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::SizeChanged(size) => {
+ self.state.size = size;
+ }
+ Message::AngleChanged(angle) => {
+ self.state.angle = angle;
+ }
+ Message::ScaleChanged(scale) => {
+ self.state.scale = scale;
+ }
+ Message::ToggleJapanese(use_japanese) => {
+ self.state.use_japanese = use_japanese;
+ }
+ }
+
+ self.state.cache.clear();
+ }
+
+ fn view(&self) -> Element<Message> {
+ let slider_with_label = |label, range, value, message: fn(f32) -> _| {
+ column![
+ row![
+ text(label),
+ horizontal_space(Length::Fill),
+ text(format!("{:.2}", value))
+ ],
+ slider(range, value, message).step(0.01)
+ ]
+ .spacing(2)
+ };
+
+ column![
+ canvas(&self.state).width(Length::Fill).height(Length::Fill),
+ column![
+ checkbox(
+ "Use Japanese",
+ self.state.use_japanese,
+ Message::ToggleJapanese
+ ),
+ row![
+ slider_with_label(
+ "Size",
+ 2.0..=80.0,
+ self.state.size,
+ Message::SizeChanged,
+ ),
+ slider_with_label(
+ "Angle",
+ 0.0..=360.0,
+ self.state.angle,
+ Message::AngleChanged,
+ ),
+ slider_with_label(
+ "Scale",
+ 1.0..=20.0,
+ self.state.scale,
+ Message::ScaleChanged,
+ ),
+ ]
+ .spacing(20),
+ ]
+ .align_items(Alignment::Center)
+ .spacing(10)
+ ]
+ .spacing(10)
+ .padding(20)
+ .into()
+ }
+
+ fn theme(&self) -> Theme {
+ Theme::Dark
+ }
+}
+
+struct State {
+ size: f32,
+ angle: f32,
+ scale: f32,
+ use_japanese: bool,
+ cache: canvas::Cache,
+}
+
+impl State {
+ pub fn new() -> Self {
+ Self {
+ size: 40.0,
+ angle: 0.0,
+ scale: 1.0,
+ use_japanese: false,
+ cache: canvas::Cache::new(),
+ }
+ }
+}
+
+impl<Message> canvas::Program<Message> for State {
+ type State = ();
+
+ fn draw(
+ &self,
+ _state: &Self::State,
+ renderer: &Renderer,
+ theme: &Theme,
+ bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ ) -> Vec<canvas::Geometry> {
+ let geometry = self.cache.draw(renderer, bounds.size(), |frame| {
+ let palette = theme.palette();
+ let center = bounds.center();
+
+ frame.translate(Vector::new(center.x, center.y));
+ frame.scale(self.scale);
+ frame.rotate(self.angle * std::f32::consts::PI / 180.0);
+
+ frame.fill_text(canvas::Text {
+ position: Point::new(0.0, 0.0),
+ color: palette.text,
+ size: self.size.into(),
+ content: String::from(if self.use_japanese {
+ "ベクトルテキスト🎉"
+ } else {
+ "Vectorial Text! 🎉"
+ }),
+ horizontal_alignment: alignment::Horizontal::Center,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: text::Shaping::Advanced,
+ ..canvas::Text::default()
+ });
+ });
+
+ vec![geometry]
+ }
+}
diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs
index 697badb4..fdf1e0f9 100644
--- a/examples/visible_bounds/src/main.rs
+++ b/examples/visible_bounds/src/main.rs
@@ -167,7 +167,7 @@ impl Application for Example {
Event::Mouse(mouse::Event::CursorMoved { position }) => {
Some(Message::MouseMoved(position))
}
- Event::Window(window::Event::Resized { .. }) => {
+ Event::Window(_, window::Event::Resized { .. }) => {
Some(Message::WindowResized)
}
_ => None,
diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml
index 2756e8e0..8f1b876a 100644
--- a/examples/websocket/Cargo.toml
+++ b/examples/websocket/Cargo.toml
@@ -13,7 +13,7 @@ once_cell.workspace = true
warp = "0.3"
[dependencies.async-tungstenite]
-version = "0.23"
+version = "0.24"
features = ["tokio-rustls-webpki-roots"]
[dependencies.tokio]
diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs
index 920189f5..38a6db1e 100644
--- a/examples/websocket/src/main.rs
+++ b/examples/websocket/src/main.rs
@@ -3,7 +3,7 @@ mod echo;
use iced::alignment::{self, Alignment};
use iced::executor;
use iced::widget::{
- button, column, container, row, scrollable, text, text_input, Column,
+ button, column, container, row, scrollable, text, text_input,
};
use iced::{
Application, Color, Command, Element, Length, Settings, Subscription, Theme,
@@ -108,15 +108,9 @@ impl Application for WebSocket {
.into()
} else {
scrollable(
- Column::with_children(
- self.messages
- .iter()
- .cloned()
- .map(text)
- .map(Element::from)
- .collect(),
+ column(
+ self.messages.iter().cloned().map(text).map(Element::from),
)
- .width(Length::Fill)
.spacing(10),
)
.id(MESSAGE_LOG.clone())
@@ -131,7 +125,7 @@ impl Application for WebSocket {
let mut button = button(
text("Send")
- .height(Length::Fill)
+ .height(40)
.vertical_alignment(alignment::Vertical::Center),
)
.padding([0, 20]);
@@ -149,7 +143,6 @@ impl Application for WebSocket {
};
column![message_log, new_message_input]
- .width(Length::Fill)
.height(Length::Fill)
.padding(20)
.spacing(10)