Merge branch 'master' into remove-vertex-indexing

This commit is contained in:
Héctor Ramón Jiménez 2024-01-19 20:41:52 +01:00
commit 1781068e1c
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
240 changed files with 12776 additions and 3395 deletions

View file

@ -17,8 +17,6 @@ clippy --workspace --no-deps -- \
-D clippy::useless_conversion
"""
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
nitpick = """
clippy --workspace --no-deps -- \
-D warnings \

View file

@ -25,7 +25,6 @@ body:
Before filing an issue...
- If you are using `wgpu`, you need an environment that supports Vulkan, Metal, or DirectX 12. Please, make sure you can run [the `wgpu` examples].
- If you are using `glow`, you need support for OpenGL 2.1+. Please, make sure you can run [the `glow` examples].
If you have any issues running any of the examples, make sure your graphics drivers are up-to-date. If the issues persist, please report them to the authors of the libraries directly!

View file

@ -12,6 +12,8 @@ jobs:
- name: Install cargo-audit
run: cargo install cargo-audit
- uses: actions/checkout@master
- name: Resolve dependencies
run: cargo update
- name: Audit vulnerabilities
run: cargo audit
@ -22,5 +24,7 @@ jobs:
- name: Install cargo-outdated
run: cargo install cargo-outdated
- uses: actions/checkout@master
- name: Delete `web-sys` dependency from `integration` example
run: sed -i '$d' examples/integration/Cargo.toml
- name: Find outdated dependencies
run: cargo outdated --workspace --exit-code 1
run: cargo outdated --workspace --exit-code 1 --ignore raw-window-handle

29
.github/workflows/check.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Check
on: [push, pull_request]
jobs:
widget:
runs-on: ubuntu-latest
steps:
- uses: hecrj/setup-rust-action@v1
- uses: actions/checkout@master
- name: Check standalone `iced_widget` crate
run: cargo check --package iced_widget --features image,svg,canvas
wasm:
runs-on: ubuntu-latest
env:
RUSTFLAGS: --cfg=web_sys_unstable_apis
steps:
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
targets: wasm32-unknown-unknown
- uses: actions/checkout@master
- name: Run checks
run: cargo check --package iced --target wasm32-unknown-unknown
- name: Check compilation of `tour` example
run: cargo build --package tour --target wasm32-unknown-unknown
- name: Check compilation of `todos` example
run: cargo build --package todos --target wasm32-unknown-unknown
- name: Check compilation of `integration` example
run: cargo build --package integration --target wasm32-unknown-unknown

View file

@ -8,13 +8,14 @@ jobs:
steps:
- uses: hecrj/setup-rust-action@v1
with:
rust-version: nightly
rust-version: nightly-2023-12-11
- uses: actions/checkout@v2
- name: Generate documentation
run: |
RUSTDOCFLAGS="--cfg docsrs" \
cargo doc --no-deps --all-features \
-p iced_core \
-p iced_highlighter \
-p iced_style \
-p iced_futures \
-p iced_runtime \

View file

@ -2,7 +2,7 @@ name: Lint
on: [push, pull_request]
jobs:
all:
runs-on: ubuntu-latest
runs-on: macOS-latest
steps:
- uses: hecrj/setup-rust-action@v1
with:

View file

@ -1,8 +1,10 @@
name: Test
on: [push, pull_request]
jobs:
native:
all:
runs-on: ${{ matrix.os }}
env:
RUSTFLAGS: --deny warnings
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
@ -17,27 +19,8 @@ jobs:
run: |
export DEBIAN_FRONTED=noninteractive
sudo apt-get -qq update
sudo apt-get install -y libxkbcommon-dev
sudo apt-get install -y libxkbcommon-dev libgtk-3-dev
- name: Run tests
run: |
cargo test --verbose --workspace
cargo test --verbose --workspace --all-features
web:
runs-on: ubuntu-latest
env:
RUSTFLAGS: --cfg=web_sys_unstable_apis
steps:
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
targets: wasm32-unknown-unknown
- uses: actions/checkout@master
- name: Run checks
run: cargo check --package iced --target wasm32-unknown-unknown
- name: Check compilation of `tour` example
run: cargo build --package tour --target wasm32-unknown-unknown
- name: Check compilation of `todos` example
run: cargo build --package todos --target wasm32-unknown-unknown
- name: Check compilation of `integration` example
run: cargo build --package integration --target wasm32-unknown-unknown

View file

@ -20,7 +20,7 @@ maintenance = { status = "actively-developed" }
[features]
default = ["wgpu"]
# Enable the `wgpu` GPU-accelerated renderer backend
wgpu = ["iced_renderer/wgpu"]
wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"]
# Enables the `Image` widget
image = ["iced_widget/image", "dep:image"]
# Enables the `Svg` widget
@ -47,6 +47,10 @@ system = ["iced_winit/system"]
web-colors = ["iced_renderer/web-colors"]
# Enables the WebGL backend, replacing WebGPU
webgl = ["iced_renderer/webgl"]
# Enables the syntax `highlighter` module
highlighter = ["iced_highlighter"]
# Enables experimental multi-window support.
multi-window = ["iced_winit/multi-window"]
# Enables the advanced module
advanced = []
@ -58,6 +62,9 @@ iced_widget.workspace = true
iced_winit.features = ["application"]
iced_winit.workspace = true
iced_highlighter.workspace = true
iced_highlighter.optional = true
thiserror.workspace = true
image.workspace = true
@ -78,8 +85,9 @@ members = [
"core",
"futures",
"graphics",
"runtime",
"highlighter",
"renderer",
"runtime",
"style",
"tiny_skia",
"wgpu",
@ -103,6 +111,7 @@ iced = { version = "0.12", path = "." }
iced_core = { version = "0.12", path = "core" }
iced_futures = { version = "0.12", path = "futures" }
iced_graphics = { version = "0.12", path = "graphics" }
iced_highlighter = { version = "0.12", path = "highlighter" }
iced_renderer = { version = "0.12", path = "renderer" }
iced_runtime = { version = "0.12", path = "runtime" }
iced_style = { version = "0.12", path = "style" }
@ -114,14 +123,13 @@ iced_winit = { version = "0.12", path = "winit" }
async-std = "1.0"
bitflags = "1.0"
bytemuck = { version = "1.0", features = ["derive"] }
cosmic-text = "0.9"
cosmic-text = "0.10"
futures = "0.3"
glam = "0.24"
glyphon = { git = "https://github.com/grovesNL/glyphon.git", rev = "20f0f8fa80e0d0df4c63634ce9176fa489546ca9" }
glyphon = "0.5"
guillotiere = "0.6"
half = "2.2"
image = "0.24"
instant = "0.1"
kamadak-exif = "0.5"
kurbo = "0.9"
log = "0.4"
@ -132,22 +140,25 @@ once_cell = "1.0"
ouroboros = "0.17"
palette = "0.7"
qrcode = { version = "0.12", default-features = false }
raw-window-handle = "0.5"
resvg = "0.35"
raw-window-handle = "0.6"
resvg = "0.36"
rustc-hash = "1.0"
smol = "1.0"
softbuffer = "0.2"
smol_str = "0.2"
softbuffer = "0.4"
syntect = "5.1"
sysinfo = "0.28"
thiserror = "1.0"
tiny-skia = "0.10"
tiny-skia = "0.11"
tokio = "1.0"
tracing = "0.1"
twox-hash = { version = "1.0", default-features = false }
xxhash-rust = { version = "0.8", features = ["xxh3"] }
unicode-segmentation = "1.0"
wasm-bindgen-futures = "0.4"
wasm-timer = "0.2"
web-sys = "0.3"
wgpu = "0.17"
web-time = "0.2"
wgpu = "0.19"
winapi = "0.3"
window_clipboard = "0.3"
winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e", default-features = false }
window_clipboard = "0.4"
winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5" }

View file

@ -45,7 +45,7 @@ The widgets of a _graphical_ user interface produce some primitives that eventua
Currently, there are two different official renderers:
- [`iced_wgpu`] is powered by [`wgpu`] and supports Vulkan, DirectX 12, and Metal.
- [`iced_glow`] is powered by [`glow`] and supports OpenGL 2.1+ and OpenGL ES 2.0+.
- [`tiny-skia`] is used as a fallback software renderer when `wgpu` is not supported.
Additionally, the [`iced_graphics`] subcrate contains a bunch of backend-agnostic types that can be leveraged to build renderers. Both of the renderers rely on the graphical foundations provided by this crate.
@ -54,10 +54,7 @@ The widgets of a graphical user _interface_ are interactive. __Shells__ gather a
Normally, a shell will be responsible of creating a window and managing the lifecycle of a user interface, implementing a runtime of [The Elm Architecture].
As of now, there are two official shells:
- [`iced_winit`] implements a shell runtime on top of [`winit`].
- [`iced_glutin`] is similar to [`iced_winit`], but it also deals with [OpenGL context creation].
As of now, there is one official shell: [`iced_winit`] implements a shell runtime on top of [`winit`].
## The web target
The Web platform provides all the abstractions necessary to draw widgets and gather users interactions.
@ -91,5 +88,4 @@ Finally, [`iced`] unifies everything into a simple abstraction to create cross-p
[`winit`]: https://github.com/rust-windowing/winit
[`glutin`]: https://github.com/rust-windowing/glutin
[`dodrio`]: https://github.com/fitzgen/dodrio
[OpenGL context creation]: https://www.khronos.org/opengl/wiki/Creating_an_OpenGL_Context
[The Elm Architecture]: https://guide.elm-lang.org/architecture/

View file

@ -9,7 +9,7 @@
[![License](https://img.shields.io/crates/l/iced.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE)
[![Downloads](https://img.shields.io/crates/d/iced.svg)](https://crates.io/crates/iced)
[![Test Status](https://img.shields.io/github/actions/workflow/status/iced-rs/iced/test.yml?branch=master&event=push&label=test)](https://github.com/iced-rs/iced/actions)
[![Discourse](https://img.shields.io/discourse/users?server=https%3A%2F%2Fdiscourse.iced.rs&color=5e7ce2)](https://discourse.iced.rs/)
[![Discourse](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscourse.iced.rs%2Fsite%2Fstatistics.json&query=%24.users_count&suffix=%20users&label=discourse&color=5e7ce2)](https://discourse.iced.rs/)
[![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd)
A cross-platform GUI library for Rust focused on simplicity and type-safety.

View file

@ -13,15 +13,17 @@ keywords.workspace = true
[dependencies]
bitflags.workspace = true
log.workspace = true
thiserror.workspace = true
twox-hash.workspace = true
num-traits.workspace = true
smol_str.workspace = true
thiserror.workspace = true
web-time.workspace = true
xxhash-rust.workspace = true
palette.workspace = true
palette.optional = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
instant.workspace = true
[target.'cfg(windows)'.dependencies]
raw-window-handle.workspace = true
[dev-dependencies]
approx = "0.5"

View file

@ -89,6 +89,26 @@ impl Color {
}
}
/// Creates a [`Color`] from its linear RGBA components.
pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
// As described in:
// https://en.wikipedia.org/wiki/SRGB
fn gamma_component(u: f32) -> f32 {
if u < 0.0031308 {
12.92 * u
} else {
1.055 * u.powf(1.0 / 2.4) - 0.055
}
}
Self {
r: gamma_component(r),
g: gamma_component(g),
b: gamma_component(b),
a,
}
}
/// Converts the [`Color`] into its RGBA8 equivalent.
#[must_use]
pub fn into_rgba8(self) -> [u8; 4] {

View file

@ -6,7 +6,7 @@ use crate::renderer;
use crate::widget;
use crate::widget::tree::{self, Tree};
use crate::{
Clipboard, Color, Layout, Length, Rectangle, Shell, Vector, Widget,
Clipboard, Color, Layout, Length, Rectangle, Shell, Size, Vector, Widget,
};
use std::any::Any;
@ -296,12 +296,8 @@ where
self.widget.diff(tree);
}
fn width(&self) -> Length {
self.widget.width()
}
fn height(&self) -> Length {
self.widget.height()
fn size(&self) -> Size<Length> {
self.widget.size()
}
fn layout(
@ -466,12 +462,8 @@ impl<'a, Message, Renderer> Widget<Message, Renderer>
where
Renderer: crate::Renderer,
{
fn width(&self) -> Length {
self.element.widget.width()
}
fn height(&self) -> Length {
self.element.widget.height()
fn size(&self) -> Size<Length> {
self.element.widget.size()
}
fn tag(&self) -> tree::Tag {

View file

@ -19,7 +19,7 @@ pub enum Event {
Mouse(mouse::Event),
/// A window event
Window(window::Event),
Window(window::Id, window::Event),
/// A touch event
Touch(touch::Event),

View file

@ -12,8 +12,6 @@ pub struct Font {
pub stretch: Stretch,
/// The [`Style`] of the [`Font`].
pub style: Style,
/// Whether if the [`Font`] is monospaced or not.
pub monospaced: bool,
}
impl Font {
@ -23,13 +21,11 @@ impl Font {
weight: Weight::Normal,
stretch: Stretch::Normal,
style: Style::Normal,
monospaced: false,
};
/// A monospaced font with normal [`Weight`].
pub const MONOSPACE: Font = Font {
family: Family::Monospace,
monospaced: true,
..Self::DEFAULT
};

View file

@ -1,6 +1,7 @@
/// The hasher used to compare layouts.
#[derive(Debug, Default)]
pub struct Hasher(twox_hash::XxHash64);
#[allow(missing_debug_implementations)] // Doesn't really make sense to have debug on the hasher state anyways.
#[derive(Default)]
pub struct Hasher(xxhash_rust::xxh3::Xxh3);
impl core::hash::Hasher for Hasher {
fn write(&mut self, bytes: &[u8]) {

View file

@ -164,6 +164,16 @@ impl std::fmt::Debug for Data {
}
}
/// Image filtering strategy.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FilterMethod {
/// Bilinear interpolation.
#[default]
Linear,
/// Nearest neighbor.
Nearest,
}
/// A [`Renderer`] that can render raster graphics.
///
/// [renderer]: crate::renderer
@ -178,5 +188,10 @@ pub trait Renderer: crate::Renderer {
/// Draws an image with the given [`Handle`] and inside the provided
/// `bounds`.
fn draw(&mut self, handle: Self::Handle, bounds: Rectangle);
fn draw(
&mut self,
handle: Self::Handle,
filter_method: FilterMethod,
bounds: Rectangle,
);
}

View file

@ -1,8 +1,11 @@
//! Listen to keyboard events.
pub mod key;
mod event;
mod key_code;
mod location;
mod modifiers;
pub use event::Event;
pub use key_code::KeyCode;
pub use key::Key;
pub use location::Location;
pub use modifiers::Modifiers;

View file

@ -1,4 +1,5 @@
use super::{KeyCode, Modifiers};
use crate::keyboard::{Key, Location, Modifiers};
use crate::SmolStr;
/// A keyboard event.
///
@ -6,29 +7,35 @@ use super::{KeyCode, Modifiers};
/// additional events, feel free to [open an issue] and share your use case!_
///
/// [open an issue]: https://github.com/iced-rs/iced/issues
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event {
/// A keyboard key was pressed.
KeyPressed {
/// The key identifier
key_code: KeyCode,
/// The key pressed.
key: Key,
/// The state of the modifier keys
/// The location of the key.
location: Location,
/// The state of the modifier keys.
modifiers: Modifiers,
/// The text produced by the key press, if any.
text: Option<SmolStr>,
},
/// A keyboard key was released.
KeyReleased {
/// The key identifier
key_code: KeyCode,
/// The key released.
key: Key,
/// The state of the modifier keys
/// The location of the key.
location: Location,
/// The state of the modifier keys.
modifiers: Modifiers,
},
/// A unicode character was received.
CharacterReceived(char),
/// The keyboard modifiers have changed.
ModifiersChanged(Modifiers),
}

744
core/src/keyboard/key.rs Normal file
View file

@ -0,0 +1,744 @@
//! Identify keyboard keys.
use crate::SmolStr;
/// A key on the keyboard.
///
/// This is mostly the `Key` type found in [`winit`].
///
/// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Key<C = SmolStr> {
/// A key with an established name.
Named(Named),
/// A key string that corresponds to the character typed by the user, taking into account the
/// users current locale setting, and any system-level keyboard mapping overrides that are in
/// effect.
Character(C),
/// An unidentified key.
Unidentified,
}
impl Key {
/// Convert `Key::Character(SmolStr)` to `Key::Character(&str)` so you can more easily match on
/// `Key`. All other variants remain unchanged.
pub fn as_ref(&self) -> Key<&str> {
match self {
Self::Named(named) => Key::Named(*named),
Self::Character(c) => Key::Character(c.as_ref()),
Self::Unidentified => Key::Unidentified,
}
}
}
/// A named key.
///
/// This is mostly the `NamedKey` type found in [`winit`].
///
/// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[allow(missing_docs)]
pub enum Named {
/// The `Alt` (Alternative) key.
///
/// This key enables the alternate modifier function for interpreting concurrent or subsequent
/// keyboard input. This key value is also used for the Apple <kbd>Option</kbd> key.
Alt,
/// The Alternate Graphics (<kbd>AltGr</kbd> or <kbd>AltGraph</kbd>) key.
///
/// This key is used enable the ISO Level 3 shift modifier (the standard `Shift` key is the
/// level 2 modifier).
AltGraph,
/// The `Caps Lock` (Capital) key.
///
/// Toggle capital character lock function for interpreting subsequent keyboard input event.
CapsLock,
/// The `Control` or `Ctrl` key.
///
/// Used to enable control modifier function for interpreting concurrent or subsequent keyboard
/// input.
Control,
/// The Function switch `Fn` key. Activating this key simultaneously with another key changes
/// that keys value to an alternate character or function. This key is often handled directly
/// in the keyboard hardware and does not usually generate key events.
Fn,
/// The Function-Lock (`FnLock` or `F-Lock`) key. Activating this key switches the mode of the
/// keyboard to changes some keys' values to an alternate character or function. This key is
/// often handled directly in the keyboard hardware and does not usually generate key events.
FnLock,
/// The `NumLock` or Number Lock key. Used to toggle numpad mode function for interpreting
/// subsequent keyboard input.
NumLock,
/// Toggle between scrolling and cursor movement modes.
ScrollLock,
/// Used to enable shift modifier function for interpreting concurrent or subsequent keyboard
/// input.
Shift,
/// The Symbol modifier key (used on some virtual keyboards).
Symbol,
SymbolLock,
// Legacy modifier key. Also called "Super" in certain places.
Meta,
// Legacy modifier key.
Hyper,
/// Used to enable "super" modifier function for interpreting concurrent or subsequent keyboard
/// input. This key value is used for the "Windows Logo" key and the Apple `Command` or `⌘` key.
///
/// Note: In some contexts (e.g. the Web) this is referred to as the "Meta" key.
Super,
/// The `Enter` or `↵` key. Used to activate current selection or accept current input. This key
/// value is also used for the `Return` (Macintosh numpad) key. This key value is also used for
/// the Android `KEYCODE_DPAD_CENTER`.
Enter,
/// The Horizontal Tabulation `Tab` key.
Tab,
/// Used in text to insert a space between words. Usually located below the character keys.
Space,
/// Navigate or traverse downward. (`KEYCODE_DPAD_DOWN`)
ArrowDown,
/// Navigate or traverse leftward. (`KEYCODE_DPAD_LEFT`)
ArrowLeft,
/// Navigate or traverse rightward. (`KEYCODE_DPAD_RIGHT`)
ArrowRight,
/// Navigate or traverse upward. (`KEYCODE_DPAD_UP`)
ArrowUp,
/// The End key, used with keyboard entry to go to the end of content (`KEYCODE_MOVE_END`).
End,
/// The Home key, used with keyboard entry, to go to start of content (`KEYCODE_MOVE_HOME`).
/// For the mobile phone `Home` key (which goes to the phones main screen), use [`GoHome`].
///
/// [`GoHome`]: Self::GoHome
Home,
/// Scroll down or display next page of content.
PageDown,
/// Scroll up or display previous page of content.
PageUp,
/// Used to remove the character to the left of the cursor. This key value is also used for
/// the key labeled `Delete` on MacOS keyboards.
Backspace,
/// Remove the currently selected input.
Clear,
/// Copy the current selection. (`APPCOMMAND_COPY`)
Copy,
/// The Cursor Select key.
CrSel,
/// Cut the current selection. (`APPCOMMAND_CUT`)
Cut,
/// Used to delete the character to the right of the cursor. This key value is also used for the
/// key labeled `Delete` on MacOS keyboards when `Fn` is active.
Delete,
/// The Erase to End of Field key. This key deletes all characters from the current cursor
/// position to the end of the current field.
EraseEof,
/// The Extend Selection (Exsel) key.
ExSel,
/// Toggle between text modes for insertion or overtyping.
/// (`KEYCODE_INSERT`)
Insert,
/// The Paste key. (`APPCOMMAND_PASTE`)
Paste,
/// Redo the last action. (`APPCOMMAND_REDO`)
Redo,
/// Undo the last action. (`APPCOMMAND_UNDO`)
Undo,
/// The Accept (Commit, OK) key. Accept current option or input method sequence conversion.
Accept,
/// Redo or repeat an action.
Again,
/// The Attention (Attn) key.
Attn,
Cancel,
/// Show the applications context menu.
/// This key is commonly found between the right `Super` key and the right `Control` key.
ContextMenu,
/// The `Esc` key. This key was originally used to initiate an escape sequence, but is
/// now more generally used to exit or "escape" the current context, such as closing a dialog
/// or exiting full screen mode.
Escape,
Execute,
/// Open the Find dialog. (`APPCOMMAND_FIND`)
Find,
/// Open a help dialog or toggle display of help information. (`APPCOMMAND_HELP`,
/// `KEYCODE_HELP`)
Help,
/// Pause the current state or application (as appropriate).
///
/// Note: Do not use this value for the `Pause` button on media controllers. Use `"MediaPause"`
/// instead.
Pause,
/// Play or resume the current state or application (as appropriate).
///
/// Note: Do not use this value for the `Play` button on media controllers. Use `"MediaPlay"`
/// instead.
Play,
/// The properties (Props) key.
Props,
Select,
/// The ZoomIn key. (`KEYCODE_ZOOM_IN`)
ZoomIn,
/// The ZoomOut key. (`KEYCODE_ZOOM_OUT`)
ZoomOut,
/// The Brightness Down key. Typically controls the display brightness.
/// (`KEYCODE_BRIGHTNESS_DOWN`)
BrightnessDown,
/// The Brightness Up key. Typically controls the display brightness. (`KEYCODE_BRIGHTNESS_UP`)
BrightnessUp,
/// Toggle removable media to eject (open) and insert (close) state. (`KEYCODE_MEDIA_EJECT`)
Eject,
LogOff,
/// Toggle power state. (`KEYCODE_POWER`)
/// Note: Note: Some devices might not expose this key to the operating environment.
Power,
/// The `PowerOff` key. Sometime called `PowerDown`.
PowerOff,
/// Initiate print-screen function.
PrintScreen,
/// The Hibernate key. This key saves the current state of the computer to disk so that it can
/// be restored. The computer will then shutdown.
Hibernate,
/// The Standby key. This key turns off the display and places the computer into a low-power
/// mode without completely shutting down. It is sometimes labelled `Suspend` or `Sleep` key.
/// (`KEYCODE_SLEEP`)
Standby,
/// The WakeUp key. (`KEYCODE_WAKEUP`)
WakeUp,
/// Initate the multi-candidate mode.
AllCandidates,
Alphanumeric,
/// Initiate the Code Input mode to allow characters to be entered by
/// their code points.
CodeInput,
/// The Compose key, also known as "Multi_key" on the X Window System. This key acts in a
/// manner similar to a dead key, triggering a mode where subsequent key presses are combined to
/// produce a different character.
Compose,
/// Convert the current input method sequence.
Convert,
/// The Final Mode `Final` key used on some Asian keyboards, to enable the final mode for IMEs.
FinalMode,
/// Switch to the first character group. (ISO/IEC 9995)
GroupFirst,
/// Switch to the last character group. (ISO/IEC 9995)
GroupLast,
/// Switch to the next character group. (ISO/IEC 9995)
GroupNext,
/// Switch to the previous character group. (ISO/IEC 9995)
GroupPrevious,
/// Toggle between or cycle through input modes of IMEs.
ModeChange,
NextCandidate,
/// Accept current input method sequence without
/// conversion in IMEs.
NonConvert,
PreviousCandidate,
Process,
SingleCandidate,
/// Toggle between Hangul and English modes.
HangulMode,
HanjaMode,
JunjaMode,
/// The Eisu key. This key may close the IME, but its purpose is defined by the current IME.
/// (`KEYCODE_EISU`)
Eisu,
/// The (Half-Width) Characters key.
Hankaku,
/// The Hiragana (Japanese Kana characters) key.
Hiragana,
/// The Hiragana/Katakana toggle key. (`KEYCODE_KATAKANA_HIRAGANA`)
HiraganaKatakana,
/// The Kana Mode (Kana Lock) key. This key is used to enter hiragana mode (typically from
/// romaji mode).
KanaMode,
/// The Kanji (Japanese name for ideographic characters of Chinese origin) Mode key. This key is
/// typically used to switch to a hiragana keyboard for the purpose of converting input into
/// kanji. (`KEYCODE_KANA`)
KanjiMode,
/// The Katakana (Japanese Kana characters) key.
Katakana,
/// The Roman characters function key.
Romaji,
/// The Zenkaku (Full-Width) Characters key.
Zenkaku,
/// The Zenkaku/Hankaku (full-width/half-width) toggle key. (`KEYCODE_ZENKAKU_HANKAKU`)
ZenkakuHankaku,
/// General purpose virtual function key, as index 1.
Soft1,
/// General purpose virtual function key, as index 2.
Soft2,
/// General purpose virtual function key, as index 3.
Soft3,
/// General purpose virtual function key, as index 4.
Soft4,
/// Select next (numerically or logically) lower channel. (`APPCOMMAND_MEDIA_CHANNEL_DOWN`,
/// `KEYCODE_CHANNEL_DOWN`)
ChannelDown,
/// Select next (numerically or logically) higher channel. (`APPCOMMAND_MEDIA_CHANNEL_UP`,
/// `KEYCODE_CHANNEL_UP`)
ChannelUp,
/// Close the current document or message (Note: This doesnt close the application).
/// (`APPCOMMAND_CLOSE`)
Close,
/// Open an editor to forward the current message. (`APPCOMMAND_FORWARD_MAIL`)
MailForward,
/// Open an editor to reply to the current message. (`APPCOMMAND_REPLY_TO_MAIL`)
MailReply,
/// Send the current message. (`APPCOMMAND_SEND_MAIL`)
MailSend,
/// Close the current media, for example to close a CD or DVD tray. (`KEYCODE_MEDIA_CLOSE`)
MediaClose,
/// Initiate or continue forward playback at faster than normal speed, or increase speed if
/// already fast forwarding. (`APPCOMMAND_MEDIA_FAST_FORWARD`, `KEYCODE_MEDIA_FAST_FORWARD`)
MediaFastForward,
/// Pause the currently playing media. (`APPCOMMAND_MEDIA_PAUSE`, `KEYCODE_MEDIA_PAUSE`)
///
/// Note: Media controller devices should use this value rather than `"Pause"` for their pause
/// keys.
MediaPause,
/// Initiate or continue media playback at normal speed, if not currently playing at normal
/// speed. (`APPCOMMAND_MEDIA_PLAY`, `KEYCODE_MEDIA_PLAY`)
MediaPlay,
/// Toggle media between play and pause states. (`APPCOMMAND_MEDIA_PLAY_PAUSE`,
/// `KEYCODE_MEDIA_PLAY_PAUSE`)
MediaPlayPause,
/// Initiate or resume recording of currently selected media. (`APPCOMMAND_MEDIA_RECORD`,
/// `KEYCODE_MEDIA_RECORD`)
MediaRecord,
/// Initiate or continue reverse playback at faster than normal speed, or increase speed if
/// already rewinding. (`APPCOMMAND_MEDIA_REWIND`, `KEYCODE_MEDIA_REWIND`)
MediaRewind,
/// Stop media playing, pausing, forwarding, rewinding, or recording, if not already stopped.
/// (`APPCOMMAND_MEDIA_STOP`, `KEYCODE_MEDIA_STOP`)
MediaStop,
/// Seek to next media or program track. (`APPCOMMAND_MEDIA_NEXTTRACK`, `KEYCODE_MEDIA_NEXT`)
MediaTrackNext,
/// Seek to previous media or program track. (`APPCOMMAND_MEDIA_PREVIOUSTRACK`,
/// `KEYCODE_MEDIA_PREVIOUS`)
MediaTrackPrevious,
/// Open a new document or message. (`APPCOMMAND_NEW`)
New,
/// Open an existing document or message. (`APPCOMMAND_OPEN`)
Open,
/// Print the current document or message. (`APPCOMMAND_PRINT`)
Print,
/// Save the current document or message. (`APPCOMMAND_SAVE`)
Save,
/// Spellcheck the current document or selection. (`APPCOMMAND_SPELL_CHECK`)
SpellCheck,
/// The `11` key found on media numpads that
/// have buttons from `1` ... `12`.
Key11,
/// The `12` key found on media numpads that
/// have buttons from `1` ... `12`.
Key12,
/// Adjust audio balance leftward. (`VK_AUDIO_BALANCE_LEFT`)
AudioBalanceLeft,
/// Adjust audio balance rightward. (`VK_AUDIO_BALANCE_RIGHT`)
AudioBalanceRight,
/// Decrease audio bass boost or cycle down through bass boost states. (`APPCOMMAND_BASS_DOWN`,
/// `VK_BASS_BOOST_DOWN`)
AudioBassBoostDown,
/// Toggle bass boost on/off. (`APPCOMMAND_BASS_BOOST`)
AudioBassBoostToggle,
/// Increase audio bass boost or cycle up through bass boost states. (`APPCOMMAND_BASS_UP`,
/// `VK_BASS_BOOST_UP`)
AudioBassBoostUp,
/// Adjust audio fader towards front. (`VK_FADER_FRONT`)
AudioFaderFront,
/// Adjust audio fader towards rear. (`VK_FADER_REAR`)
AudioFaderRear,
/// Advance surround audio mode to next available mode. (`VK_SURROUND_MODE_NEXT`)
AudioSurroundModeNext,
/// Decrease treble. (`APPCOMMAND_TREBLE_DOWN`)
AudioTrebleDown,
/// Increase treble. (`APPCOMMAND_TREBLE_UP`)
AudioTrebleUp,
/// Decrease audio volume. (`APPCOMMAND_VOLUME_DOWN`, `KEYCODE_VOLUME_DOWN`)
AudioVolumeDown,
/// Increase audio volume. (`APPCOMMAND_VOLUME_UP`, `KEYCODE_VOLUME_UP`)
AudioVolumeUp,
/// Toggle between muted state and prior volume level. (`APPCOMMAND_VOLUME_MUTE`,
/// `KEYCODE_VOLUME_MUTE`)
AudioVolumeMute,
/// Toggle the microphone on/off. (`APPCOMMAND_MIC_ON_OFF_TOGGLE`)
MicrophoneToggle,
/// Decrease microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_DOWN`)
MicrophoneVolumeDown,
/// Increase microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_UP`)
MicrophoneVolumeUp,
/// Mute the microphone. (`APPCOMMAND_MICROPHONE_VOLUME_MUTE`, `KEYCODE_MUTE`)
MicrophoneVolumeMute,
/// Show correction list when a word is incorrectly identified. (`APPCOMMAND_CORRECTION_LIST`)
SpeechCorrectionList,
/// Toggle between dictation mode and command/control mode.
/// (`APPCOMMAND_DICTATE_OR_COMMAND_CONTROL_TOGGLE`)
SpeechInputToggle,
/// The first generic "LaunchApplication" key. This is commonly associated with launching "My
/// Computer", and may have a computer symbol on the key. (`APPCOMMAND_LAUNCH_APP1`)
LaunchApplication1,
/// The second generic "LaunchApplication" key. This is commonly associated with launching
/// "Calculator", and may have a calculator symbol on the key. (`APPCOMMAND_LAUNCH_APP2`,
/// `KEYCODE_CALCULATOR`)
LaunchApplication2,
/// The "Calendar" key. (`KEYCODE_CALENDAR`)
LaunchCalendar,
/// The "Contacts" key. (`KEYCODE_CONTACTS`)
LaunchContacts,
/// The "Mail" key. (`APPCOMMAND_LAUNCH_MAIL`)
LaunchMail,
/// The "Media Player" key. (`APPCOMMAND_LAUNCH_MEDIA_SELECT`)
LaunchMediaPlayer,
LaunchMusicPlayer,
LaunchPhone,
LaunchScreenSaver,
LaunchSpreadsheet,
LaunchWebBrowser,
LaunchWebCam,
LaunchWordProcessor,
/// Navigate to previous content or page in current history. (`APPCOMMAND_BROWSER_BACKWARD`)
BrowserBack,
/// Open the list of browser favorites. (`APPCOMMAND_BROWSER_FAVORITES`)
BrowserFavorites,
/// Navigate to next content or page in current history. (`APPCOMMAND_BROWSER_FORWARD`)
BrowserForward,
/// Go to the users preferred home page. (`APPCOMMAND_BROWSER_HOME`)
BrowserHome,
/// Refresh the current page or content. (`APPCOMMAND_BROWSER_REFRESH`)
BrowserRefresh,
/// Call up the users preferred search page. (`APPCOMMAND_BROWSER_SEARCH`)
BrowserSearch,
/// Stop loading the current page or content. (`APPCOMMAND_BROWSER_STOP`)
BrowserStop,
/// The Application switch key, which provides a list of recent apps to switch between.
/// (`KEYCODE_APP_SWITCH`)
AppSwitch,
/// The Call key. (`KEYCODE_CALL`)
Call,
/// The Camera key. (`KEYCODE_CAMERA`)
Camera,
/// The Camera focus key. (`KEYCODE_FOCUS`)
CameraFocus,
/// The End Call key. (`KEYCODE_ENDCALL`)
EndCall,
/// The Back key. (`KEYCODE_BACK`)
GoBack,
/// The Home key, which goes to the phones main screen. (`KEYCODE_HOME`)
GoHome,
/// The Headset Hook key. (`KEYCODE_HEADSETHOOK`)
HeadsetHook,
LastNumberRedial,
/// The Notification key. (`KEYCODE_NOTIFICATION`)
Notification,
/// Toggle between manner mode state: silent, vibrate, ring, ... (`KEYCODE_MANNER_MODE`)
MannerMode,
VoiceDial,
/// Switch to viewing TV. (`KEYCODE_TV`)
TV,
/// TV 3D Mode. (`KEYCODE_3D_MODE`)
TV3DMode,
/// Toggle between antenna and cable input. (`KEYCODE_TV_ANTENNA_CABLE`)
TVAntennaCable,
/// Audio description. (`KEYCODE_TV_AUDIO_DESCRIPTION`)
TVAudioDescription,
/// Audio description mixing volume down. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN`)
TVAudioDescriptionMixDown,
/// Audio description mixing volume up. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP`)
TVAudioDescriptionMixUp,
/// Contents menu. (`KEYCODE_TV_CONTENTS_MENU`)
TVContentsMenu,
/// Contents menu. (`KEYCODE_TV_DATA_SERVICE`)
TVDataService,
/// Switch the input mode on an external TV. (`KEYCODE_TV_INPUT`)
TVInput,
/// Switch to component input #1. (`KEYCODE_TV_INPUT_COMPONENT_1`)
TVInputComponent1,
/// Switch to component input #2. (`KEYCODE_TV_INPUT_COMPONENT_2`)
TVInputComponent2,
/// Switch to composite input #1. (`KEYCODE_TV_INPUT_COMPOSITE_1`)
TVInputComposite1,
/// Switch to composite input #2. (`KEYCODE_TV_INPUT_COMPOSITE_2`)
TVInputComposite2,
/// Switch to HDMI input #1. (`KEYCODE_TV_INPUT_HDMI_1`)
TVInputHDMI1,
/// Switch to HDMI input #2. (`KEYCODE_TV_INPUT_HDMI_2`)
TVInputHDMI2,
/// Switch to HDMI input #3. (`KEYCODE_TV_INPUT_HDMI_3`)
TVInputHDMI3,
/// Switch to HDMI input #4. (`KEYCODE_TV_INPUT_HDMI_4`)
TVInputHDMI4,
/// Switch to VGA input #1. (`KEYCODE_TV_INPUT_VGA_1`)
TVInputVGA1,
/// Media context menu. (`KEYCODE_TV_MEDIA_CONTEXT_MENU`)
TVMediaContext,
/// Toggle network. (`KEYCODE_TV_NETWORK`)
TVNetwork,
/// Number entry. (`KEYCODE_TV_NUMBER_ENTRY`)
TVNumberEntry,
/// Toggle the power on an external TV. (`KEYCODE_TV_POWER`)
TVPower,
/// Radio. (`KEYCODE_TV_RADIO_SERVICE`)
TVRadioService,
/// Satellite. (`KEYCODE_TV_SATELLITE`)
TVSatellite,
/// Broadcast Satellite. (`KEYCODE_TV_SATELLITE_BS`)
TVSatelliteBS,
/// Communication Satellite. (`KEYCODE_TV_SATELLITE_CS`)
TVSatelliteCS,
/// Toggle between available satellites. (`KEYCODE_TV_SATELLITE_SERVICE`)
TVSatelliteToggle,
/// Analog Terrestrial. (`KEYCODE_TV_TERRESTRIAL_ANALOG`)
TVTerrestrialAnalog,
/// Digital Terrestrial. (`KEYCODE_TV_TERRESTRIAL_DIGITAL`)
TVTerrestrialDigital,
/// Timer programming. (`KEYCODE_TV_TIMER_PROGRAMMING`)
TVTimer,
/// Switch the input mode on an external AVR (audio/video receiver). (`KEYCODE_AVR_INPUT`)
AVRInput,
/// Toggle the power on an external AVR (audio/video receiver). (`KEYCODE_AVR_POWER`)
AVRPower,
/// General purpose color-coded media function key, as index 0 (red). (`VK_COLORED_KEY_0`,
/// `KEYCODE_PROG_RED`)
ColorF0Red,
/// General purpose color-coded media function key, as index 1 (green). (`VK_COLORED_KEY_1`,
/// `KEYCODE_PROG_GREEN`)
ColorF1Green,
/// General purpose color-coded media function key, as index 2 (yellow). (`VK_COLORED_KEY_2`,
/// `KEYCODE_PROG_YELLOW`)
ColorF2Yellow,
/// General purpose color-coded media function key, as index 3 (blue). (`VK_COLORED_KEY_3`,
/// `KEYCODE_PROG_BLUE`)
ColorF3Blue,
/// General purpose color-coded media function key, as index 4 (grey). (`VK_COLORED_KEY_4`)
ColorF4Grey,
/// General purpose color-coded media function key, as index 5 (brown). (`VK_COLORED_KEY_5`)
ColorF5Brown,
/// Toggle the display of Closed Captions. (`VK_CC`, `KEYCODE_CAPTIONS`)
ClosedCaptionToggle,
/// Adjust brightness of device, by toggling between or cycling through states. (`VK_DIMMER`)
Dimmer,
/// Swap video sources. (`VK_DISPLAY_SWAP`)
DisplaySwap,
/// Select Digital Video Rrecorder. (`KEYCODE_DVR`)
DVR,
/// Exit the current application. (`VK_EXIT`)
Exit,
/// Clear program or content stored as favorite 0. (`VK_CLEAR_FAVORITE_0`)
FavoriteClear0,
/// Clear program or content stored as favorite 1. (`VK_CLEAR_FAVORITE_1`)
FavoriteClear1,
/// Clear program or content stored as favorite 2. (`VK_CLEAR_FAVORITE_2`)
FavoriteClear2,
/// Clear program or content stored as favorite 3. (`VK_CLEAR_FAVORITE_3`)
FavoriteClear3,
/// Select (recall) program or content stored as favorite 0. (`VK_RECALL_FAVORITE_0`)
FavoriteRecall0,
/// Select (recall) program or content stored as favorite 1. (`VK_RECALL_FAVORITE_1`)
FavoriteRecall1,
/// Select (recall) program or content stored as favorite 2. (`VK_RECALL_FAVORITE_2`)
FavoriteRecall2,
/// Select (recall) program or content stored as favorite 3. (`VK_RECALL_FAVORITE_3`)
FavoriteRecall3,
/// Store current program or content as favorite 0. (`VK_STORE_FAVORITE_0`)
FavoriteStore0,
/// Store current program or content as favorite 1. (`VK_STORE_FAVORITE_1`)
FavoriteStore1,
/// Store current program or content as favorite 2. (`VK_STORE_FAVORITE_2`)
FavoriteStore2,
/// Store current program or content as favorite 3. (`VK_STORE_FAVORITE_3`)
FavoriteStore3,
/// Toggle display of program or content guide. (`VK_GUIDE`, `KEYCODE_GUIDE`)
Guide,
/// If guide is active and displayed, then display next days content. (`VK_NEXT_DAY`)
GuideNextDay,
/// If guide is active and displayed, then display previous days content. (`VK_PREV_DAY`)
GuidePreviousDay,
/// Toggle display of information about currently selected context or media. (`VK_INFO`,
/// `KEYCODE_INFO`)
Info,
/// Toggle instant replay. (`VK_INSTANT_REPLAY`)
InstantReplay,
/// Launch linked content, if available and appropriate. (`VK_LINK`)
Link,
/// List the current program. (`VK_LIST`)
ListProgram,
/// Toggle display listing of currently available live content or programs. (`VK_LIVE`)
LiveContent,
/// Lock or unlock current content or program. (`VK_LOCK`)
Lock,
/// Show a list of media applications: audio/video players and image viewers. (`VK_APPS`)
///
/// Note: Do not confuse this key value with the Windows' `VK_APPS` / `VK_CONTEXT_MENU` key,
/// which is encoded as `"ContextMenu"`.
MediaApps,
/// Audio track key. (`KEYCODE_MEDIA_AUDIO_TRACK`)
MediaAudioTrack,
/// Select previously selected channel or media. (`VK_LAST`, `KEYCODE_LAST_CHANNEL`)
MediaLast,
/// Skip backward to next content or program. (`KEYCODE_MEDIA_SKIP_BACKWARD`)
MediaSkipBackward,
/// Skip forward to next content or program. (`VK_SKIP`, `KEYCODE_MEDIA_SKIP_FORWARD`)
MediaSkipForward,
/// Step backward to next content or program. (`KEYCODE_MEDIA_STEP_BACKWARD`)
MediaStepBackward,
/// Step forward to next content or program. (`KEYCODE_MEDIA_STEP_FORWARD`)
MediaStepForward,
/// Media top menu. (`KEYCODE_MEDIA_TOP_MENU`)
MediaTopMenu,
/// Navigate in. (`KEYCODE_NAVIGATE_IN`)
NavigateIn,
/// Navigate to next key. (`KEYCODE_NAVIGATE_NEXT`)
NavigateNext,
/// Navigate out. (`KEYCODE_NAVIGATE_OUT`)
NavigateOut,
/// Navigate to previous key. (`KEYCODE_NAVIGATE_PREVIOUS`)
NavigatePrevious,
/// Cycle to next favorite channel (in favorites list). (`VK_NEXT_FAVORITE_CHANNEL`)
NextFavoriteChannel,
/// Cycle to next user profile (if there are multiple user profiles). (`VK_USER`)
NextUserProfile,
/// Access on-demand content or programs. (`VK_ON_DEMAND`)
OnDemand,
/// Pairing key to pair devices. (`KEYCODE_PAIRING`)
Pairing,
/// Move picture-in-picture window down. (`VK_PINP_DOWN`)
PinPDown,
/// Move picture-in-picture window. (`VK_PINP_MOVE`)
PinPMove,
/// Toggle display of picture-in-picture window. (`VK_PINP_TOGGLE`)
PinPToggle,
/// Move picture-in-picture window up. (`VK_PINP_UP`)
PinPUp,
/// Decrease media playback speed. (`VK_PLAY_SPEED_DOWN`)
PlaySpeedDown,
/// Reset playback to normal speed. (`VK_PLAY_SPEED_RESET`)
PlaySpeedReset,
/// Increase media playback speed. (`VK_PLAY_SPEED_UP`)
PlaySpeedUp,
/// Toggle random media or content shuffle mode. (`VK_RANDOM_TOGGLE`)
RandomToggle,
/// Not a physical key, but this key code is sent when the remote control battery is low.
/// (`VK_RC_LOW_BATTERY`)
RcLowBattery,
/// Toggle or cycle between media recording speeds. (`VK_RECORD_SPEED_NEXT`)
RecordSpeedNext,
/// Toggle RF (radio frequency) input bypass mode (pass RF input directly to the RF output).
/// (`VK_RF_BYPASS`)
RfBypass,
/// Toggle scan channels mode. (`VK_SCAN_CHANNELS_TOGGLE`)
ScanChannelsToggle,
/// Advance display screen mode to next available mode. (`VK_SCREEN_MODE_NEXT`)
ScreenModeNext,
/// Toggle display of device settings screen. (`VK_SETTINGS`, `KEYCODE_SETTINGS`)
Settings,
/// Toggle split screen mode. (`VK_SPLIT_SCREEN_TOGGLE`)
SplitScreenToggle,
/// Switch the input mode on an external STB (set top box). (`KEYCODE_STB_INPUT`)
STBInput,
/// Toggle the power on an external STB (set top box). (`KEYCODE_STB_POWER`)
STBPower,
/// Toggle display of subtitles, if available. (`VK_SUBTITLE`)
Subtitle,
/// Toggle display of teletext, if available (`VK_TELETEXT`, `KEYCODE_TV_TELETEXT`).
Teletext,
/// Advance video mode to next available mode. (`VK_VIDEO_MODE_NEXT`)
VideoModeNext,
/// Cause device to identify itself in some manner, e.g., audibly or visibly. (`VK_WINK`)
Wink,
/// Toggle between full-screen and scaled content, or alter magnification level. (`VK_ZOOM`,
/// `KEYCODE_TV_ZOOM_MODE`)
ZoomToggle,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F1,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F2,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F3,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F4,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F5,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F6,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F7,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F8,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F9,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F10,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F11,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F12,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F13,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F14,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F15,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F16,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F17,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F18,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F19,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F20,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F21,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F22,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F23,
/// General-purpose function key.
/// Usually found at the top of the keyboard.
F24,
/// General-purpose function key.
F25,
/// General-purpose function key.
F26,
/// General-purpose function key.
F27,
/// General-purpose function key.
F28,
/// General-purpose function key.
F29,
/// General-purpose function key.
F30,
/// General-purpose function key.
F31,
/// General-purpose function key.
F32,
/// General-purpose function key.
F33,
/// General-purpose function key.
F34,
/// General-purpose function key.
F35,
}

View file

@ -1,203 +0,0 @@
/// The symbolic name of a keyboard key.
///
/// This is mostly the `KeyCode` type found in [`winit`].
///
/// [`winit`]: https://docs.rs/winit/0.20.0-alpha3/winit/
#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)]
#[repr(u32)]
#[allow(missing_docs)]
pub enum KeyCode {
/// The '1' key over the letters.
Key1,
/// The '2' key over the letters.
Key2,
/// The '3' key over the letters.
Key3,
/// The '4' key over the letters.
Key4,
/// The '5' key over the letters.
Key5,
/// The '6' key over the letters.
Key6,
/// The '7' key over the letters.
Key7,
/// The '8' key over the letters.
Key8,
/// The '9' key over the letters.
Key9,
/// The '0' key over the 'O' and 'P' keys.
Key0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
/// The Escape key, next to F1.
Escape,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
F21,
F22,
F23,
F24,
/// Print Screen/SysRq.
Snapshot,
/// Scroll Lock.
Scroll,
/// Pause/Break key, next to Scroll lock.
Pause,
/// `Insert`, next to Backspace.
Insert,
Home,
Delete,
End,
PageDown,
PageUp,
Left,
Up,
Right,
Down,
/// The Backspace key, right over Enter.
Backspace,
/// The Enter key.
Enter,
/// The space bar.
Space,
/// The "Compose" key on Linux.
Compose,
Caret,
Numlock,
Numpad0,
Numpad1,
Numpad2,
Numpad3,
Numpad4,
Numpad5,
Numpad6,
Numpad7,
Numpad8,
Numpad9,
NumpadAdd,
NumpadDivide,
NumpadDecimal,
NumpadComma,
NumpadEnter,
NumpadEquals,
NumpadMultiply,
NumpadSubtract,
AbntC1,
AbntC2,
Apostrophe,
Apps,
Asterisk,
At,
Ax,
Backslash,
Calculator,
Capital,
Colon,
Comma,
Convert,
Equals,
Grave,
Kana,
Kanji,
LAlt,
LBracket,
LControl,
LShift,
LWin,
Mail,
MediaSelect,
MediaStop,
Minus,
Mute,
MyComputer,
NavigateForward, // also called "Next"
NavigateBackward, // also called "Prior"
NextTrack,
NoConvert,
OEM102,
Period,
PlayPause,
Plus,
Power,
PrevTrack,
RAlt,
RBracket,
RControl,
RShift,
RWin,
Semicolon,
Slash,
Sleep,
Stop,
Sysrq,
Tab,
Underline,
Unlabeled,
VolumeDown,
VolumeUp,
Wake,
WebBack,
WebFavorites,
WebForward,
WebHome,
WebRefresh,
WebSearch,
WebStop,
Yen,
Copy,
Paste,
Cut,
}

View file

@ -0,0 +1,12 @@
/// The location of a key on the keyboard.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Location {
/// The standard group of keys on the keyboard.
Standard,
/// The left side of the keyboard.
Left,
/// The right side of the keyboard.
Right,
/// The numpad of the keyboard.
Numpad,
}

View file

@ -7,7 +7,7 @@ pub mod flex;
pub use limits::Limits;
pub use node::Node;
use crate::{Point, Rectangle, Size, Vector};
use crate::{Length, Padding, Point, Rectangle, Size, Vector};
/// The bounds of a [`Node`] and its children, using absolute coordinates.
#[derive(Debug, Clone, Copy)]
@ -71,12 +71,12 @@ pub fn next_to_each_other(
left: impl FnOnce(&Limits) -> Node,
right: impl FnOnce(&Limits) -> Node,
) -> Node {
let mut left_node = left(limits);
let left_node = left(limits);
let left_size = left_node.size();
let right_limits = limits.shrink(Size::new(left_size.width + spacing, 0.0));
let mut right_node = right(&right_limits);
let right_node = right(&right_limits);
let right_size = right_node.size();
let (left_y, right_y) = if left_size.height > right_size.height {
@ -85,14 +85,106 @@ pub fn next_to_each_other(
((right_size.height - left_size.height) / 2.0, 0.0)
};
left_node.move_to(Point::new(0.0, left_y));
right_node.move_to(Point::new(left_size.width + spacing, right_y));
Node::with_children(
Size::new(
left_size.width + spacing + right_size.width,
left_size.height.max(right_size.height),
),
vec![left_node, right_node],
vec![
left_node.move_to(Point::new(0.0, left_y)),
right_node.move_to(Point::new(left_size.width + spacing, right_y)),
],
)
}
/// Computes the resulting [`Node`] that fits the [`Limits`] given
/// some width and height requirements and no intrinsic size.
pub fn atomic(
limits: &Limits,
width: impl Into<Length>,
height: impl Into<Length>,
) -> Node {
let width = width.into();
let height = height.into();
Node::new(limits.resolve(width, height, Size::ZERO))
}
/// Computes the resulting [`Node`] that fits the [`Limits`] given
/// some width and height requirements and a closure that produces
/// the intrinsic [`Size`] inside the given [`Limits`].
pub fn sized(
limits: &Limits,
width: impl Into<Length>,
height: impl Into<Length>,
f: impl FnOnce(&Limits) -> Size,
) -> Node {
let width = width.into();
let height = height.into();
let limits = limits.width(width).height(height);
let intrinsic_size = f(&limits);
Node::new(limits.resolve(width, height, intrinsic_size))
}
/// Computes the resulting [`Node`] that fits the [`Limits`] given
/// some width and height requirements and a closure that produces
/// the content [`Node`] inside the given [`Limits`].
pub fn contained(
limits: &Limits,
width: impl Into<Length>,
height: impl Into<Length>,
f: impl FnOnce(&Limits) -> Node,
) -> Node {
let width = width.into();
let height = height.into();
let limits = limits.width(width).height(height);
let content = f(&limits);
Node::with_children(
limits.resolve(width, height, content.size()),
vec![content],
)
}
/// Computes the [`Node`] that fits the [`Limits`] given some width, height, and
/// [`Padding`] requirements and a closure that produces the content [`Node`]
/// inside the given [`Limits`].
pub fn padded(
limits: &Limits,
width: impl Into<Length>,
height: impl Into<Length>,
padding: impl Into<Padding>,
layout: impl FnOnce(&Limits) -> Node,
) -> Node {
positioned(limits, width, height, padding, layout, |content, _| content)
}
/// Computes a [`padded`] [`Node`] with a positioning step.
pub fn positioned(
limits: &Limits,
width: impl Into<Length>,
height: impl Into<Length>,
padding: impl Into<Padding>,
layout: impl FnOnce(&Limits) -> Node,
position: impl FnOnce(Node, Size) -> Node,
) -> Node {
let width = width.into();
let height = height.into();
let padding = padding.into();
let limits = limits.width(width).height(height);
let content = layout(&limits.shrink(padding));
let padding = padding.fit(content.size(), limits.max());
let size = limits
.shrink(padding)
.resolve(width, height, content.size());
Node::with_children(
size.expand(padding),
vec![position(content.move_to((padding.left, padding.top)), size)],
)
}

View file

@ -20,7 +20,7 @@ use crate::Element;
use crate::layout::{Limits, Node};
use crate::widget;
use crate::{Alignment, Padding, Point, Size};
use crate::{Alignment, Length, Padding, Point, Size};
/// The main axis of a flex layout.
#[derive(Debug)]
@ -47,7 +47,7 @@ impl Axis {
}
}
fn pack(&self, main: f32, cross: f32) -> (f32, f32) {
fn pack<T>(&self, main: T, cross: T) -> (T, T) {
match self {
Axis::Horizontal => (main, cross),
Axis::Vertical => (cross, main),
@ -63,6 +63,8 @@ pub fn resolve<Message, Renderer>(
axis: Axis,
renderer: &Renderer,
limits: &Limits,
width: Length,
height: Length,
padding: Padding,
spacing: f32,
align_items: Alignment,
@ -72,26 +74,64 @@ pub fn resolve<Message, Renderer>(
where
Renderer: crate::Renderer,
{
let limits = limits.pad(padding);
let limits = limits.width(width).height(height).shrink(padding);
let total_spacing = spacing * items.len().saturating_sub(1) as f32;
let max_cross = axis.cross(limits.max());
let mut fill_sum = 0;
let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill()));
let mut fill_main_sum = 0;
let mut cross = match axis {
Axis::Horizontal => match height {
Length::Shrink => 0.0,
_ => max_cross,
},
Axis::Vertical => match width {
Length::Shrink => 0.0,
_ => max_cross,
},
};
let mut available = axis.main(limits.max()) - total_spacing;
let mut nodes: Vec<Node> = Vec::with_capacity(items.len());
nodes.resize(items.len(), Node::default());
for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() {
let fill_factor = match axis {
Axis::Horizontal => child.as_widget().width(),
Axis::Vertical => child.as_widget().height(),
}
.fill_factor();
let (fill_main_factor, fill_cross_factor) = {
let size = child.as_widget().size();
if fill_factor == 0 {
let (max_width, max_height) = axis.pack(available, max_cross);
axis.pack(size.width.fill_factor(), size.height.fill_factor())
};
if fill_main_factor == 0 {
if fill_cross_factor == 0 {
let (max_width, max_height) = axis.pack(available, max_cross);
let child_limits =
Limits::new(Size::ZERO, Size::new(max_width, max_height));
let layout =
child.as_widget().layout(tree, renderer, &child_limits);
let size = layout.size();
available -= axis.main(size);
cross = cross.max(axis.cross(size));
nodes[i] = layout;
}
} else {
fill_main_sum += fill_main_factor;
}
}
for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() {
let (fill_main_factor, fill_cross_factor) = {
let size = child.as_widget().size();
axis.pack(size.width.fill_factor(), size.height.fill_factor())
};
if fill_main_factor == 0 && fill_cross_factor != 0 {
let (max_width, max_height) = axis.pack(available, cross);
let child_limits =
Limits::new(Size::ZERO, Size::new(max_width, max_height));
@ -101,34 +141,47 @@ where
let size = layout.size();
available -= axis.main(size);
cross = cross.max(axis.cross(size));
cross = cross.max(axis.cross(layout.size()));
nodes[i] = layout;
} else {
fill_sum += fill_factor;
}
}
let remaining = available.max(0.0);
let remaining = match axis {
Axis::Horizontal => match width {
Length::Shrink => 0.0,
_ => available.max(0.0),
},
Axis::Vertical => match height {
Length::Shrink => 0.0,
_ => available.max(0.0),
},
};
for (i, (child, tree)) in items.iter().zip(trees).enumerate() {
let fill_factor = match axis {
Axis::Horizontal => child.as_widget().width(),
Axis::Vertical => child.as_widget().height(),
}
.fill_factor();
let (fill_main_factor, fill_cross_factor) = {
let size = child.as_widget().size();
axis.pack(size.width.fill_factor(), size.height.fill_factor())
};
if fill_main_factor != 0 {
let max_main =
remaining * fill_main_factor as f32 / fill_main_sum as f32;
if fill_factor != 0 {
let max_main = remaining * fill_factor as f32 / fill_sum as f32;
let min_main = if max_main.is_infinite() {
0.0
} else {
max_main
};
let (min_width, min_height) =
axis.pack(min_main, axis.cross(limits.min()));
let max_cross = if fill_cross_factor == 0 {
max_cross
} else {
cross
};
let (min_width, min_height) = axis.pack(min_main, 0.0);
let (max_width, max_height) = axis.pack(max_main, max_cross);
let child_limits = Limits::new(
@ -154,18 +207,18 @@ where
let (x, y) = axis.pack(main, pad.1);
node.move_to(Point::new(x, y));
node.move_to_mut(Point::new(x, y));
match axis {
Axis::Horizontal => {
node.align(
node.align_mut(
Alignment::Start,
align_items,
Size::new(0.0, cross),
);
}
Axis::Vertical => {
node.align(
node.align_mut(
align_items,
Alignment::Start,
Size::new(cross, 0.0),
@ -178,8 +231,12 @@ where
main += axis.main(size);
}
let (width, height) = axis.pack(main - pad.0, cross);
let size = limits.resolve(Size::new(width, height));
let (intrinsic_width, intrinsic_height) = axis.pack(main - pad.0, cross);
let size = limits.resolve(
width,
height,
Size::new(intrinsic_width, intrinsic_height),
);
Node::with_children(size.pad(padding), nodes)
Node::with_children(size.expand(padding), nodes)
}

View file

@ -1,12 +1,11 @@
#![allow(clippy::manual_clamp)]
use crate::{Length, Padding, Size};
use crate::{Length, Size};
/// A set of size constraints for layouting.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Limits {
min: Size,
max: Size,
fill: Size,
}
impl Limits {
@ -14,16 +13,11 @@ impl Limits {
pub const NONE: Limits = Limits {
min: Size::ZERO,
max: Size::INFINITY,
fill: Size::INFINITY,
};
/// Creates new [`Limits`] with the given minimum and maximum [`Size`].
pub const fn new(min: Size, max: Size) -> Limits {
Limits {
min,
max,
fill: Size::INFINITY,
}
Limits { min, max }
}
/// Returns the minimum [`Size`] of the [`Limits`].
@ -36,26 +30,15 @@ impl Limits {
self.max
}
/// Returns the fill [`Size`] of the [`Limits`].
pub fn fill(&self) -> Size {
self.fill
}
/// Applies a width constraint to the current [`Limits`].
pub fn width(mut self, width: impl Into<Length>) -> Limits {
match width.into() {
Length::Shrink => {
self.fill.width = self.min.width;
}
Length::Fill | Length::FillPortion(_) => {
self.fill.width = self.fill.width.min(self.max.width);
}
Length::Shrink | Length::Fill | Length::FillPortion(_) => {}
Length::Fixed(amount) => {
let new_width = amount.min(self.max.width).max(self.min.width);
self.min.width = new_width;
self.max.width = new_width;
self.fill.width = new_width;
}
}
@ -65,19 +48,13 @@ impl Limits {
/// Applies a height constraint to the current [`Limits`].
pub fn height(mut self, height: impl Into<Length>) -> Limits {
match height.into() {
Length::Shrink => {
self.fill.height = self.min.height;
}
Length::Fill | Length::FillPortion(_) => {
self.fill.height = self.fill.height.min(self.max.height);
}
Length::Shrink | Length::Fill | Length::FillPortion(_) => {}
Length::Fixed(amount) => {
let new_height =
amount.min(self.max.height).max(self.min.height);
self.min.height = new_height;
self.max.height = new_height;
self.fill.height = new_height;
}
}
@ -112,13 +89,10 @@ impl Limits {
self
}
/// Shrinks the current [`Limits`] to account for the given padding.
pub fn pad(&self, padding: Padding) -> Limits {
self.shrink(Size::new(padding.horizontal(), padding.vertical()))
}
/// Shrinks the current [`Limits`] by the given [`Size`].
pub fn shrink(&self, size: Size) -> Limits {
pub fn shrink(&self, size: impl Into<Size>) -> Limits {
let size = size.into();
let min = Size::new(
(self.min().width - size.width).max(0.0),
(self.min().height - size.height).max(0.0),
@ -129,12 +103,7 @@ impl Limits {
(self.max().height - size.height).max(0.0),
);
let fill = Size::new(
(self.fill.width - size.width).max(0.0),
(self.fill.height - size.height).max(0.0),
);
Limits { min, max, fill }
Limits { min, max }
}
/// Removes the minimum width constraint for the current [`Limits`].
@ -142,22 +111,39 @@ impl Limits {
Limits {
min: Size::ZERO,
max: self.max,
fill: self.fill,
}
}
/// Computes the resulting [`Size`] that fits the [`Limits`] given the
/// intrinsic size of some content.
pub fn resolve(&self, intrinsic_size: Size) -> Size {
Size::new(
intrinsic_size
.width
.min(self.max.width)
.max(self.fill.width),
intrinsic_size
/// Computes the resulting [`Size`] that fits the [`Limits`] given
/// some width and height requirements and the intrinsic size of
/// some content.
pub fn resolve(
&self,
width: impl Into<Length>,
height: impl Into<Length>,
intrinsic_size: Size,
) -> Size {
let width = match width.into() {
Length::Fill | Length::FillPortion(_) => self.max.width,
Length::Fixed(amount) => {
amount.min(self.max.width).max(self.min.width)
}
Length::Shrink => {
intrinsic_size.width.min(self.max.width).max(self.min.width)
}
};
let height = match height.into() {
Length::Fill | Length::FillPortion(_) => self.max.height,
Length::Fixed(amount) => {
amount.min(self.max.height).max(self.min.height)
}
Length::Shrink => intrinsic_size
.height
.min(self.max.height)
.max(self.fill.height),
)
.max(self.min.height),
};
Size::new(width, height)
}
}

View file

@ -1,4 +1,4 @@
use crate::{Alignment, Point, Rectangle, Size, Vector};
use crate::{Alignment, Padding, Point, Rectangle, Size, Vector};
/// The bounds of an element and its children.
#[derive(Debug, Clone, Default)]
@ -26,6 +26,14 @@ impl Node {
}
}
/// Creates a new [`Node`] that wraps a single child with some [`Padding`].
pub fn container(child: Self, padding: Padding) -> Self {
Self::with_children(
child.bounds.size().expand(padding),
vec![child.move_to(Point::new(padding.left, padding.top))],
)
}
/// Returns the [`Size`] of the [`Node`].
pub fn size(&self) -> Size {
Size::new(self.bounds.width, self.bounds.height)
@ -43,6 +51,17 @@ impl Node {
/// Aligns the [`Node`] in the given space.
pub fn align(
mut self,
horizontal_alignment: Alignment,
vertical_alignment: Alignment,
space: Size,
) -> Self {
self.align_mut(horizontal_alignment, vertical_alignment, space);
self
}
/// Mutable reference version of [`Self::align`].
pub fn align_mut(
&mut self,
horizontal_alignment: Alignment,
vertical_alignment: Alignment,
@ -70,13 +89,23 @@ impl Node {
}
/// Moves the [`Node`] to the given position.
pub fn move_to(&mut self, position: Point) {
pub fn move_to(mut self, position: impl Into<Point>) -> Self {
self.move_to_mut(position);
self
}
/// Mutable reference version of [`Self::move_to`].
pub fn move_to_mut(&mut self, position: impl Into<Point>) {
let position = position.into();
self.bounds.x = position.x;
self.bounds.y = position.y;
}
/// Translates the [`Node`] by the given translation.
pub fn translate(self, translation: Vector) -> Self {
pub fn translate(self, translation: impl Into<Vector>) -> Self {
let translation = translation.into();
Self {
bounds: self.bounds + translation,
..self

View file

@ -36,6 +36,24 @@ impl Length {
Length::Fixed(_) => 0,
}
}
/// Returns `true` iff the [`Length`] is either [`Length::Fill`] or
// [`Length::FillPortion`].
pub fn is_fill(&self) -> bool {
self.fill_factor() != 0
}
/// Returns the "fluid" variant of the [`Length`].
///
/// Specifically:
/// - [`Length::Shrink`] if [`Length::Shrink`] or [`Length::Fixed`].
/// - [`Length::Fill`] otherwise.
pub fn fluid(&self) -> Length {
match self {
Length::Fill | Length::FillPortion(_) => Length::Fill,
Length::Shrink | Length::Fixed(_) => Length::Shrink,
}
}
}
impl From<Pixels> for Length {

View file

@ -75,3 +75,5 @@ pub use size::Size;
pub use text::Text;
pub use vector::Vector;
pub use widget::Widget;
pub use smol_str::SmolStr;

View file

@ -10,6 +10,12 @@ pub enum Button {
/// The middle (wheel) button.
Middle,
/// The back mouse button.
Back,
/// The forward mouse button.
Forward,
/// Some other button.
Other(u16),
}

View file

@ -61,6 +61,11 @@ impl Click {
self.kind
}
/// Returns the position of the [`Click`].
pub fn position(&self) -> Point {
self.position
}
fn is_consecutive(&self, new_position: Point, time: Instant) -> bool {
let duration = if time > self.time {
Some(time - self.time)

View file

@ -11,7 +11,7 @@ use crate::mouse;
use crate::renderer;
use crate::widget;
use crate::widget::Tree;
use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size};
use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector};
/// An interactive component that can be displayed on top of other widgets.
pub trait Overlay<Message, Renderer>
@ -29,6 +29,7 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
translation: Vector,
) -> layout::Node;
/// Draws the [`Overlay`] using the associated `Renderer`.

View file

@ -13,6 +13,7 @@ use std::any::Any;
#[allow(missing_debug_implementations)]
pub struct Element<'a, Message, Renderer> {
position: Point,
translation: Vector,
overlay: Box<dyn Overlay<Message, Renderer> + 'a>,
}
@ -25,7 +26,11 @@ where
position: Point,
overlay: Box<dyn Overlay<Message, Renderer> + 'a>,
) -> Self {
Self { position, overlay }
Self {
position,
overlay,
translation: Vector::ZERO,
}
}
/// Returns the position of the [`Element`].
@ -36,6 +41,7 @@ where
/// Translates the [`Element`].
pub fn translate(mut self, translation: Vector) -> Self {
self.position = self.position + translation;
self.translation = self.translation + translation;
self
}
@ -48,6 +54,7 @@ where
{
Element {
position: self.position,
translation: self.translation,
overlay: Box::new(Map::new(self.overlay, f)),
}
}
@ -59,8 +66,12 @@ where
bounds: Size,
translation: Vector,
) -> layout::Node {
self.overlay
.layout(renderer, bounds, self.position + translation)
self.overlay.layout(
renderer,
bounds,
self.position + translation,
self.translation + translation,
)
}
/// Processes a runtime [`Event`].
@ -154,8 +165,9 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
translation: Vector,
) -> layout::Node {
self.content.layout(renderer, bounds, position)
self.content.layout(renderer, bounds, position, translation)
}
fn operate(

View file

@ -4,7 +4,9 @@ use crate::mouse;
use crate::overlay;
use crate::renderer;
use crate::widget;
use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size};
use crate::{
Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size, Vector,
};
/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`]
/// children.
@ -64,10 +66,9 @@ where
&mut self,
renderer: &Renderer,
bounds: Size,
position: Point,
_position: Point,
translation: Vector,
) -> layout::Node {
let translation = position - Point::ORIGIN;
layout::Node::with_children(
bounds,
self.children

View file

@ -154,3 +154,9 @@ impl From<[f32; 4]> for Padding {
}
}
}
impl From<Padding> for Size {
fn from(padding: Padding) -> Self {
Self::new(padding.horizontal(), padding.vertical())
}
}

View file

@ -1,26 +1,34 @@
use crate::Vector;
use num_traits::{Float, Num};
use std::fmt;
/// A 2D point.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Point {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Point<T = f32> {
/// The X coordinate.
pub x: f32,
pub x: T,
/// The Y coordinate.
pub y: f32,
pub y: T,
}
impl Point {
/// The origin (i.e. a [`Point`] at (0, 0)).
pub const ORIGIN: Point = Point::new(0.0, 0.0);
pub const ORIGIN: Self = Self::new(0.0, 0.0);
}
impl<T: Num> Point<T> {
/// Creates a new [`Point`] with the given coordinates.
pub const fn new(x: f32, y: f32) -> Self {
pub const fn new(x: T, y: T) -> Self {
Self { x, y }
}
/// Computes the distance to another [`Point`].
pub fn distance(&self, to: Point) -> f32 {
pub fn distance(&self, to: Self) -> T
where
T: Float,
{
let a = self.x - to.x;
let b = self.y - to.y;
@ -28,28 +36,37 @@ impl Point {
}
}
impl From<[f32; 2]> for Point {
fn from([x, y]: [f32; 2]) -> Self {
impl<T> From<[T; 2]> for Point<T>
where
T: Num,
{
fn from([x, y]: [T; 2]) -> Self {
Point { x, y }
}
}
impl From<[u16; 2]> for Point {
fn from([x, y]: [u16; 2]) -> Self {
Point::new(x.into(), y.into())
impl<T> From<(T, T)> for Point<T>
where
T: Num,
{
fn from((x, y): (T, T)) -> Self {
Self { x, y }
}
}
impl From<Point> for [f32; 2] {
fn from(point: Point) -> [f32; 2] {
impl<T> From<Point<T>> for [T; 2] {
fn from(point: Point<T>) -> [T; 2] {
[point.x, point.y]
}
}
impl std::ops::Add<Vector> for Point {
impl<T> std::ops::Add<Vector<T>> for Point<T>
where
T: std::ops::Add<Output = T>,
{
type Output = Self;
fn add(self, vector: Vector) -> Self {
fn add(self, vector: Vector<T>) -> Self {
Self {
x: self.x + vector.x,
y: self.y + vector.y,
@ -57,10 +74,13 @@ impl std::ops::Add<Vector> for Point {
}
}
impl std::ops::Sub<Vector> for Point {
impl<T> std::ops::Sub<Vector<T>> for Point<T>
where
T: std::ops::Sub<Output = T>,
{
type Output = Self;
fn sub(self, vector: Vector) -> Self {
fn sub(self, vector: Vector<T>) -> Self {
Self {
x: self.x - vector.x,
y: self.y - vector.y,
@ -68,10 +88,22 @@ impl std::ops::Sub<Vector> for Point {
}
}
impl std::ops::Sub<Point> for Point {
type Output = Vector;
impl<T> std::ops::Sub<Point<T>> for Point<T>
where
T: std::ops::Sub<Output = T>,
{
type Output = Vector<T>;
fn sub(self, point: Point) -> Vector {
fn sub(self, point: Self) -> Vector<T> {
Vector::new(self.x - point.x, self.y - point.y)
}
}
impl<T> fmt::Display for Point<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
}
}

View file

@ -43,6 +43,7 @@ impl Renderer for Null {
impl text::Renderer for Null {
type Font = Font;
type Paragraph = ();
type Editor = ();
const ICON_FONT: Font = Font::DEFAULT;
const CHECKMARK_ICON: char = '0';
@ -58,21 +59,21 @@ impl text::Renderer for Null {
fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
fn create_paragraph(&self, _text: Text<'_, Self::Font>) -> Self::Paragraph {
}
fn resize_paragraph(
&self,
_paragraph: &mut Self::Paragraph,
_new_bounds: Size,
) {
}
fn fill_paragraph(
&mut self,
_paragraph: &Self::Paragraph,
_position: Point,
_color: Color,
_clip_bounds: Rectangle,
) {
}
fn fill_editor(
&mut self,
_editor: &Self::Editor,
_position: Point,
_color: Color,
_clip_bounds: Rectangle,
) {
}
@ -81,6 +82,7 @@ impl text::Renderer for Null {
_paragraph: Text<'_, Self::Font>,
_position: Point,
_color: Color,
_clip_bounds: Rectangle,
) {
}
}
@ -88,24 +90,12 @@ impl text::Renderer for Null {
impl text::Paragraph for () {
type Font = Font;
fn content(&self) -> &str {
""
}
fn with_text(_text: Text<'_, Self::Font>) -> Self {}
fn text_size(&self) -> Pixels {
Pixels(16.0)
}
fn resize(&mut self, _new_bounds: Size) {}
fn font(&self) -> Self::Font {
Font::default()
}
fn line_height(&self) -> text::LineHeight {
text::LineHeight::default()
}
fn shaping(&self) -> text::Shaping {
text::Shaping::default()
fn compare(&self, _text: Text<'_, Self::Font>) -> text::Difference {
text::Difference::None
}
fn horizontal_alignment(&self) -> alignment::Horizontal {
@ -120,10 +110,6 @@ impl text::Paragraph for () {
None
}
fn bounds(&self) -> Size {
Size::ZERO
}
fn min_bounds(&self) -> Size {
Size::ZERO
}
@ -132,3 +118,55 @@ impl text::Paragraph for () {
None
}
}
impl text::Editor for () {
type Font = Font;
fn with_text(_text: &str) -> Self {}
fn cursor(&self) -> text::editor::Cursor {
text::editor::Cursor::Caret(Point::ORIGIN)
}
fn cursor_position(&self) -> (usize, usize) {
(0, 0)
}
fn selection(&self) -> Option<String> {
None
}
fn line(&self, _index: usize) -> Option<&str> {
None
}
fn line_count(&self) -> usize {
0
}
fn perform(&mut self, _action: text::editor::Action) {}
fn bounds(&self) -> Size {
Size::ZERO
}
fn update(
&mut self,
_new_bounds: Size,
_new_font: Self::Font,
_new_size: Pixels,
_new_line_height: text::LineHeight,
_new_highlighter: &mut impl text::Highlighter,
) {
}
fn highlight<H: text::Highlighter>(
&mut self,
_font: Self::Font,
_highlighter: &mut H,
_format_highlight: impl Fn(
&H::Highlight,
) -> text::highlighter::Format<Self::Font>,
) {
}
}

View file

@ -1,4 +1,4 @@
use crate::{Padding, Vector};
use crate::Vector;
/// An amount of space in 2 dimensions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@ -26,15 +26,7 @@ impl Size {
/// A [`Size`] with infinite width and height.
pub const INFINITY: Size = Size::new(f32::INFINITY, f32::INFINITY);
/// Increments the [`Size`] to account for the given padding.
pub fn pad(&self, padding: Padding) -> Self {
Size {
width: self.width + padding.horizontal(),
height: self.height + padding.vertical(),
}
}
/// Returns the minimum of each component of this size and another
/// Returns the minimum of each component of this size and another.
pub fn min(self, other: Self) -> Self {
Size {
width: self.width.min(other.width),
@ -42,13 +34,23 @@ impl Size {
}
}
/// Returns the maximum of each component of this size and another
/// Returns the maximum of each component of this size and another.
pub fn max(self, other: Self) -> Self {
Size {
width: self.width.max(other.width),
height: self.height.max(other.height),
}
}
/// Expands this [`Size`] by the given amount.
pub fn expand(self, other: impl Into<Size>) -> Self {
let other = other.into();
Size {
width: self.width + other.width,
height: self.height + other.height,
}
}
}
impl From<[f32; 2]> for Size {

View file

@ -1,6 +1,15 @@
//! Draw and interact with text.
mod paragraph;
pub mod editor;
pub mod highlighter;
pub use editor::Editor;
pub use highlighter::Highlighter;
pub use paragraph::Paragraph;
use crate::alignment;
use crate::{Color, Pixels, Point, Size};
use crate::{Color, Pixels, Point, Rectangle, Size};
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
@ -126,6 +135,33 @@ impl Hit {
}
}
/// The difference detected in some text.
///
/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some
/// [`Text`].
///
/// [`compare`]: Paragraph::compare
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Difference {
/// No difference.
///
/// The text can be reused as it is!
None,
/// A bounds difference.
///
/// This normally means a relayout is necessary, but the shape of the text can
/// be reused.
Bounds,
/// A shape difference.
///
/// The contents, alignment, sizes, fonts, or any other essential attributes
/// of the shape of the text have changed. A complete reshape and relayout of
/// the text is necessary.
Shape,
}
/// A renderer capable of measuring and drawing [`Text`].
pub trait Renderer: crate::Renderer {
/// The font type used.
@ -134,6 +170,9 @@ pub trait Renderer: crate::Renderer {
/// The [`Paragraph`] of this [`Renderer`].
type Paragraph: Paragraph<Font = Self::Font> + 'static;
/// The [`Editor`] of this [`Renderer`].
type Editor: Editor<Font = Self::Font> + 'static;
/// The icon font of the backend.
const ICON_FONT: Self::Font;
@ -156,33 +195,6 @@ pub trait Renderer: crate::Renderer {
/// Loads a [`Self::Font`] from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>);
/// Creates a new [`Paragraph`] laid out with the given [`Text`].
fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph;
/// Lays out the given [`Paragraph`] with some new boundaries.
fn resize_paragraph(
&self,
paragraph: &mut Self::Paragraph,
new_bounds: Size,
);
/// Updates a [`Paragraph`] to match the given [`Text`], if needed.
fn update_paragraph(
&self,
paragraph: &mut Self::Paragraph,
text: Text<'_, Self::Font>,
) {
match compare(paragraph, text) {
Difference::None => {}
Difference::Bounds => {
self.resize_paragraph(paragraph, text.bounds);
}
Difference::Shape => {
*paragraph = self.create_paragraph(text);
}
}
}
/// Draws the given [`Paragraph`] at the given position and with the given
/// [`Color`].
fn fill_paragraph(
@ -190,6 +202,17 @@ pub trait Renderer: crate::Renderer {
text: &Self::Paragraph,
position: Point,
color: Color,
clip_bounds: Rectangle,
);
/// Draws the given [`Editor`] at the given position and with the given
/// [`Color`].
fn fill_editor(
&mut self,
editor: &Self::Editor,
position: Point,
color: Color,
clip_bounds: Rectangle,
);
/// Draws the given [`Text`] at the given position and with the given
@ -199,103 +222,6 @@ pub trait Renderer: crate::Renderer {
text: Text<'_, Self::Font>,
position: Point,
color: Color,
clip_bounds: Rectangle,
);
}
/// A text paragraph.
pub trait Paragraph: Default {
/// The font of this [`Paragraph`].
type Font;
/// Returns the content of the [`Paragraph`].
fn content(&self) -> &str;
/// Returns the text size of the [`Paragraph`].
fn text_size(&self) -> Pixels;
/// Returns the [`LineHeight`] of the [`Paragraph`].
fn line_height(&self) -> LineHeight;
/// Returns the [`Self::Font`] of the [`Paragraph`].
fn font(&self) -> Self::Font;
/// Returns the [`Shaping`] strategy of the [`Paragraph`].
fn shaping(&self) -> Shaping;
/// Returns the horizontal alignment of the [`Paragraph`].
fn horizontal_alignment(&self) -> alignment::Horizontal;
/// Returns the vertical alignment of the [`Paragraph`].
fn vertical_alignment(&self) -> alignment::Vertical;
/// Returns the boundaries of the [`Paragraph`].
fn bounds(&self) -> Size;
/// Returns the minimum boundaries that can fit the contents of the
/// [`Paragraph`].
fn min_bounds(&self) -> Size;
/// Tests whether the provided point is within the boundaries of the
/// [`Paragraph`], returning information about the nearest character.
fn hit_test(&self, point: Point) -> Option<Hit>;
/// Returns the distance to the given grapheme index in the [`Paragraph`].
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>;
/// Returns the minimum width that can fit the contents of the [`Paragraph`].
fn min_width(&self) -> f32 {
self.min_bounds().width
}
/// Returns the minimum height that can fit the contents of the [`Paragraph`].
fn min_height(&self) -> f32 {
self.min_bounds().height
}
}
/// The difference detected in some text.
///
/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some
/// [`Text`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Difference {
/// No difference.
///
/// The text can be reused as it is!
None,
/// A bounds difference.
///
/// This normally means a relayout is necessary, but the shape of the text can
/// be reused.
Bounds,
/// A shape difference.
///
/// The contents, alignment, sizes, fonts, or any other essential attributes
/// of the shape of the text have changed. A complete reshape and relayout of
/// the text is necessary.
Shape,
}
/// Compares a [`Paragraph`] with some desired [`Text`] and returns the
/// [`Difference`].
pub fn compare<Font: PartialEq>(
paragraph: &impl Paragraph<Font = Font>,
text: Text<'_, Font>,
) -> Difference {
if paragraph.content() != text.content
|| paragraph.text_size() != text.size
|| paragraph.line_height().to_absolute(text.size)
!= text.line_height.to_absolute(text.size)
|| paragraph.font() != text.font
|| paragraph.shaping() != text.shaping
|| paragraph.horizontal_alignment() != text.horizontal_alignment
|| paragraph.vertical_alignment() != text.vertical_alignment
{
Difference::Shape
} else if paragraph.bounds() != text.bounds {
Difference::Bounds
} else {
Difference::None
}
}

181
core/src/text/editor.rs Normal file
View file

@ -0,0 +1,181 @@
//! Edit text.
use crate::text::highlighter::{self, Highlighter};
use crate::text::LineHeight;
use crate::{Pixels, Point, Rectangle, Size};
use std::sync::Arc;
/// A component that can be used by widgets to edit multi-line text.
pub trait Editor: Sized + Default {
/// The font of the [`Editor`].
type Font: Copy + PartialEq + Default;
/// Creates a new [`Editor`] laid out with the given text.
fn with_text(text: &str) -> Self;
/// Returns the current [`Cursor`] of the [`Editor`].
fn cursor(&self) -> Cursor;
/// Returns the current cursor position of the [`Editor`].
///
/// Line and column, respectively.
fn cursor_position(&self) -> (usize, usize);
/// Returns the current selected text of the [`Editor`].
fn selection(&self) -> Option<String>;
/// Returns the text of the given line in the [`Editor`], if it exists.
fn line(&self, index: usize) -> Option<&str>;
/// Returns the amount of lines in the [`Editor`].
fn line_count(&self) -> usize;
/// Performs an [`Action`] on the [`Editor`].
fn perform(&mut self, action: Action);
/// Returns the current boundaries of the [`Editor`].
fn bounds(&self) -> Size;
/// Updates the [`Editor`] with some new attributes.
fn update(
&mut self,
new_bounds: Size,
new_font: Self::Font,
new_size: Pixels,
new_line_height: LineHeight,
new_highlighter: &mut impl Highlighter,
);
/// Runs a text [`Highlighter`] in the [`Editor`].
fn highlight<H: Highlighter>(
&mut self,
font: Self::Font,
highlighter: &mut H,
format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>,
);
}
/// An interaction with an [`Editor`].
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
/// Apply a [`Motion`].
Move(Motion),
/// Select text with a given [`Motion`].
Select(Motion),
/// Select the word at the current cursor.
SelectWord,
/// Select the line at the current cursor.
SelectLine,
/// Perform an [`Edit`].
Edit(Edit),
/// Click the [`Editor`] at the given [`Point`].
Click(Point),
/// Drag the mouse on the [`Editor`] to the given [`Point`].
Drag(Point),
/// Scroll the [`Editor`] a certain amount of lines.
Scroll {
/// The amount of lines to scroll.
lines: i32,
},
}
impl Action {
/// Returns whether the [`Action`] is an editing action.
pub fn is_edit(&self) -> bool {
matches!(self, Self::Edit(_))
}
}
/// An action that edits text.
#[derive(Debug, Clone, PartialEq)]
pub enum Edit {
/// Insert the given character.
Insert(char),
/// Paste the given text.
Paste(Arc<String>),
/// Break the current line.
Enter,
/// Delete the previous character.
Backspace,
/// Delete the next character.
Delete,
}
/// A cursor movement.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Motion {
/// Move left.
Left,
/// Move right.
Right,
/// Move up.
Up,
/// Move down.
Down,
/// Move to the left boundary of a word.
WordLeft,
/// Move to the right boundary of a word.
WordRight,
/// Move to the start of the line.
Home,
/// Move to the end of the line.
End,
/// Move to the start of the previous window.
PageUp,
/// Move to the start of the next window.
PageDown,
/// Move to the start of the text.
DocumentStart,
/// Move to the end of the text.
DocumentEnd,
}
impl Motion {
/// Widens the [`Motion`], if possible.
pub fn widen(self) -> Self {
match self {
Self::Left => Self::WordLeft,
Self::Right => Self::WordRight,
Self::Home => Self::DocumentStart,
Self::End => Self::DocumentEnd,
_ => self,
}
}
/// Returns the [`Direction`] of the [`Motion`].
pub fn direction(&self) -> Direction {
match self {
Self::Left
| Self::Up
| Self::WordLeft
| Self::Home
| Self::PageUp
| Self::DocumentStart => Direction::Left,
Self::Right
| Self::Down
| Self::WordRight
| Self::End
| Self::PageDown
| Self::DocumentEnd => Direction::Right,
}
}
}
/// A direction in some text.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
/// <-
Left,
/// ->
Right,
}
/// The cursor of an [`Editor`].
#[derive(Debug, Clone)]
pub enum Cursor {
/// Cursor without a selection
Caret(Point),
/// Cursor selecting a range of text
Selection(Vec<Rectangle>),
}

View file

@ -0,0 +1,88 @@
//! Highlight text.
use crate::Color;
use std::ops::Range;
/// A type capable of highlighting text.
///
/// A [`Highlighter`] highlights lines in sequence. When a line changes,
/// it must be notified and the lines after the changed one must be fed
/// again to the [`Highlighter`].
pub trait Highlighter: 'static {
/// The settings to configure the [`Highlighter`].
type Settings: PartialEq + Clone;
/// The output of the [`Highlighter`].
type Highlight;
/// The highlight iterator type.
type Iterator<'a>: Iterator<Item = (Range<usize>, Self::Highlight)>
where
Self: 'a;
/// Creates a new [`Highlighter`] from its [`Self::Settings`].
fn new(settings: &Self::Settings) -> Self;
/// Updates the [`Highlighter`] with some new [`Self::Settings`].
fn update(&mut self, new_settings: &Self::Settings);
/// Notifies the [`Highlighter`] that the line at the given index has changed.
fn change_line(&mut self, line: usize);
/// Highlights the given line.
///
/// If a line changed prior to this, the first line provided here will be the
/// line that changed.
fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_>;
/// Returns the current line of the [`Highlighter`].
///
/// If `change_line` has been called, this will normally be the least index
/// that changed.
fn current_line(&self) -> usize;
}
/// A highlighter that highlights nothing.
#[derive(Debug, Clone, Copy)]
pub struct PlainText;
impl Highlighter for PlainText {
type Settings = ();
type Highlight = ();
type Iterator<'a> = std::iter::Empty<(Range<usize>, Self::Highlight)>;
fn new(_settings: &Self::Settings) -> Self {
Self
}
fn update(&mut self, _new_settings: &Self::Settings) {}
fn change_line(&mut self, _line: usize) {}
fn highlight_line(&mut self, _line: &str) -> Self::Iterator<'_> {
std::iter::empty()
}
fn current_line(&self) -> usize {
usize::MAX
}
}
/// The format of some text.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Format<Font> {
/// The [`Color`] of the text.
pub color: Option<Color>,
/// The `Font` of the text.
pub font: Option<Font>,
}
impl<Font> Default for Format<Font> {
fn default() -> Self {
Self {
color: None,
font: None,
}
}
}

View file

@ -0,0 +1,59 @@
use crate::alignment;
use crate::text::{Difference, Hit, Text};
use crate::{Point, Size};
/// A text paragraph.
pub trait Paragraph: Sized + Default {
/// The font of this [`Paragraph`].
type Font: Copy + PartialEq;
/// Creates a new [`Paragraph`] laid out with the given [`Text`].
fn with_text(text: Text<'_, Self::Font>) -> Self;
/// Lays out the [`Paragraph`] with some new boundaries.
fn resize(&mut self, new_bounds: Size);
/// Compares the [`Paragraph`] with some desired [`Text`] and returns the
/// [`Difference`].
fn compare(&self, text: Text<'_, Self::Font>) -> Difference;
/// Returns the horizontal alignment of the [`Paragraph`].
fn horizontal_alignment(&self) -> alignment::Horizontal;
/// Returns the vertical alignment of the [`Paragraph`].
fn vertical_alignment(&self) -> alignment::Vertical;
/// Returns the minimum boundaries that can fit the contents of the
/// [`Paragraph`].
fn min_bounds(&self) -> Size;
/// Tests whether the provided point is within the boundaries of the
/// [`Paragraph`], returning information about the nearest character.
fn hit_test(&self, point: Point) -> Option<Hit>;
/// Returns the distance to the given grapheme index in the [`Paragraph`].
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>;
/// Updates the [`Paragraph`] to match the given [`Text`], if needed.
fn update(&mut self, text: Text<'_, Self::Font>) {
match self.compare(text) {
Difference::None => {}
Difference::Bounds => {
self.resize(text.bounds);
}
Difference::Shape => {
*self = Self::with_text(text);
}
}
}
/// Returns the minimum width that can fit the contents of the [`Paragraph`].
fn min_width(&self) -> f32 {
self.min_bounds().width
}
/// Returns the minimum height that can fit the contents of the [`Paragraph`].
fn min_height(&self) -> f32 {
self.min_bounds().height
}
}

View file

@ -1,13 +1,4 @@
//! Keep track of time, both in native and web platforms!
#[cfg(target_arch = "wasm32")]
pub use instant::Instant;
#[cfg(target_arch = "wasm32")]
pub use instant::Duration;
#[cfg(not(target_arch = "wasm32"))]
pub use std::time::Instant;
#[cfg(not(target_arch = "wasm32"))]
pub use std::time::Duration;
pub use web_time::Duration;
pub use web_time::Instant;

View file

@ -15,7 +15,7 @@ use crate::layout::{self, Layout};
use crate::mouse;
use crate::overlay;
use crate::renderer;
use crate::{Clipboard, Length, Rectangle, Shell};
use crate::{Clipboard, Length, Rectangle, Shell, Size};
/// A component that displays information and allows interaction.
///
@ -43,11 +43,16 @@ pub trait Widget<Message, Renderer>
where
Renderer: crate::Renderer,
{
/// Returns the width of the [`Widget`].
fn width(&self) -> Length;
/// Returns the [`Size`] of the [`Widget`] in lengths.
fn size(&self) -> Size<Length>;
/// Returns the height of the [`Widget`].
fn height(&self) -> Length;
/// Returns a [`Size`] hint for laying out the [`Widget`].
///
/// This hint may be used by some widget containers to adjust their sizing strategy
/// during construction.
fn size_hint(&self) -> Size<Length> {
self.size()
}
/// Returns the [`layout::Node`] of the [`Widget`].
///

View file

@ -5,7 +5,9 @@ use crate::mouse;
use crate::renderer;
use crate::text::{self, Paragraph};
use crate::widget::tree::{self, Tree};
use crate::{Color, Element, Layout, Length, Pixels, Point, Rectangle, Widget};
use crate::{
Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget,
};
use std::borrow::Cow;
@ -134,12 +136,11 @@ where
tree::State::new(State(Renderer::Paragraph::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(
@ -172,7 +173,7 @@ where
style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: mouse::Cursor,
_viewport: &Rectangle,
viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
@ -182,6 +183,7 @@ where
layout,
state,
theme.appearance(self.style.clone()),
viewport,
);
}
}
@ -204,17 +206,15 @@ pub fn layout<Renderer>(
where
Renderer: text::Renderer,
{
let limits = limits.width(width).height(height);
let bounds = limits.max();
layout::sized(limits, width, height, |limits| {
let bounds = limits.max();
let size = size.unwrap_or_else(|| renderer.default_size());
let font = font.unwrap_or_else(|| renderer.default_font());
let size = size.unwrap_or_else(|| renderer.default_size());
let font = font.unwrap_or_else(|| renderer.default_font());
let State(ref mut paragraph) = state;
let State(ref mut paragraph) = state;
renderer.update_paragraph(
paragraph,
text::Text {
paragraph.update(text::Text {
content,
bounds,
size,
@ -223,12 +223,10 @@ where
horizontal_alignment,
vertical_alignment,
shaping,
},
);
});
let size = limits.resolve(paragraph.min_bounds());
layout::Node::new(size)
paragraph.min_bounds()
})
}
/// Draws text using the same logic as the [`Text`] widget.
@ -247,6 +245,7 @@ pub fn draw<Renderer>(
layout: Layout<'_>,
state: &State<Renderer::Paragraph>,
appearance: Appearance,
viewport: &Rectangle,
) where
Renderer: text::Renderer,
{
@ -269,6 +268,7 @@ pub fn draw<Renderer>(
paragraph,
Point::new(x, y),
appearance.color.unwrap_or(style.text_color),
*viewport,
);
}

View file

@ -67,7 +67,7 @@ impl Tree {
}
}
/// Reconciliates the children of the tree with the provided list of widgets.
/// Reconciles the children of the tree with the provided list of widgets.
pub fn diff_children<'a, Message, Renderer>(
&mut self,
new_children: &[impl Borrow<dyn Widget<Message, Renderer> + 'a>],

View file

@ -1,15 +1,21 @@
//! Build window-based GUI applications.
pub mod icon;
pub mod settings;
mod event;
mod id;
mod level;
mod mode;
mod position;
mod redraw_request;
mod user_attention;
pub use event::Event;
pub use icon::Icon;
pub use id::Id;
pub use level::Level;
pub use mode::Mode;
pub use position::Position;
pub use redraw_request::RedrawRequest;
pub use settings::Settings;
pub use user_attention::UserAttention;

View file

@ -1,10 +1,27 @@
use crate::time::Instant;
use crate::{Point, Size};
use std::path::PathBuf;
/// A window-related event.
#[derive(PartialEq, Eq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug)]
pub enum Event {
/// A window was opened.
Opened {
/// The position of the opened window. This is relative to the top-left corner of the desktop
/// the window is on, including virtual desktops. Refers to window's "inner" position,
/// or the client area, in logical pixels.
///
/// **Note**: Not available in Wayland.
position: Option<Point>,
/// The size of the created window. This is its "inner" size, or the size of the
/// client area, in logical pixels.
size: Size,
},
/// A window was closed.
Closed,
/// A window was moved.
Moved {
/// The new logical x location of the window
@ -27,9 +44,6 @@ pub enum Event {
RedrawRequested(Instant),
/// The user has requested for the window to close.
///
/// Usually, you will want to terminate the execution whenever this event
/// occurs.
CloseRequested,
/// A window was focused.
@ -44,7 +58,7 @@ pub enum Event {
/// for each file separately.
FileHovered(PathBuf),
/// A file has beend dropped into the window.
/// A file has been dropped into the window.
///
/// When the user drops multiple files at once, this event will be emitted
/// for each file separately.

21
core/src/window/id.rs Normal file
View file

@ -0,0 +1,21 @@
use std::hash::Hash;
use std::sync::atomic::{self, AtomicU64};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
/// The id of the window.
///
/// Internally Iced reserves `window::Id::MAIN` for the first window spawned.
pub struct Id(u64);
static COUNT: AtomicU64 = AtomicU64::new(1);
impl Id {
/// The reserved window [`Id`] for the first window in an Iced application.
pub const MAIN: Self = Id(0);
/// Creates a new unique window [`Id`].
pub fn unique() -> Id {
Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed))
}
}

View file

@ -1,5 +1,7 @@
use crate::Point;
/// The position of a window in a given screen.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Position {
/// The platform-specific default position for a new window.
Default,
@ -12,7 +14,7 @@ pub enum Position {
/// position. So if you have decorations enabled and want the window to be
/// at (0, 0) you would have to set the position to
/// `(PADDING_X, PADDING_Y)`.
Specific(i32, i32),
Specific(Point),
}
impl Default for Position {

View file

@ -1,21 +1,47 @@
//! Configure your windows.
#[cfg(target_os = "windows")]
#[path = "settings/windows.rs"]
mod platform;
#[cfg(target_os = "macos")]
#[path = "settings/macos.rs"]
mod platform;
#[cfg(target_os = "linux")]
#[path = "settings/linux.rs"]
mod platform;
#[cfg(target_arch = "wasm32")]
#[path = "settings/wasm.rs"]
mod platform;
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_arch = "wasm32"
)))]
#[path = "settings/other.rs"]
mod platform;
use crate::window::{Icon, Level, Position};
use crate::Size;
pub use iced_winit::settings::PlatformSpecific;
pub use platform::PlatformSpecific;
/// The window settings of an application.
#[derive(Debug, Clone)]
pub struct Settings {
/// The initial size of the window.
pub size: (u32, u32),
/// The initial logical dimensions of the window.
pub size: Size,
/// The initial position of the window.
pub position: Position,
/// The minimum size of the window.
pub min_size: Option<(u32, u32)>,
pub min_size: Option<Size>,
/// The maximum size of the window.
pub max_size: Option<(u32, u32)>,
pub max_size: Option<Size>,
/// Whether the window should be visible or not.
pub visible: bool,
@ -37,12 +63,22 @@ pub struct Settings {
/// Platform specific settings.
pub platform_specific: PlatformSpecific,
/// Whether the window will close when the user requests it, e.g. when a user presses the
/// close button.
///
/// This can be useful if you want to have some behavior that executes before the window is
/// actually destroyed. If you disable this, you must manually close the window with the
/// `window::close` command.
///
/// By default this is enabled.
pub exit_on_close_request: bool,
}
impl Default for Settings {
fn default() -> Settings {
Settings {
size: (1024, 768),
fn default() -> Self {
Self {
size: Size::new(1024.0, 768.0),
position: Position::default(),
min_size: None,
max_size: None,
@ -52,25 +88,8 @@ impl Default for Settings {
transparent: false,
level: Level::default(),
icon: None,
exit_on_close_request: true,
platform_specific: PlatformSpecific::default(),
}
}
}
impl From<Settings> for iced_winit::settings::Window {
fn from(settings: Settings) -> Self {
Self {
size: settings.size,
position: iced_winit::Position::from(settings.position),
min_size: settings.min_size,
max_size: settings.max_size,
visible: settings.visible,
resizable: settings.resizable,
decorations: settings.decorations,
transparent: settings.transparent,
level: settings.level,
icon: settings.icon.map(Icon::into),
platform_specific: settings.platform_specific,
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

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

View file

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

View file

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

Binary file not shown.

312
examples/editor/src/main.rs Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

371
examples/layout/src/main.rs Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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);
.layout(self.tree, renderer, &limits)
.align(Alignment::Center, Alignment::Center, limits.max());
child.align(Alignment::Center, Alignment::Center, limits.max());
let mut node = layout::Node::with_children(self.size, vec![child]);
node.move_to(position);
node
layout::Node::with_children(self.size, vec![child])
.move_to(position)
}
fn on_event(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more