summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml12
-rw-r--r--core/src/color.rs127
-rw-r--r--core/src/keyboard/event.rs7
-rw-r--r--core/src/keyboard/key.rs533
-rw-r--r--core/src/mouse/click.rs13
-rw-r--r--core/src/mouse/interaction.rs7
-rw-r--r--core/src/renderer/null.rs1
-rw-r--r--core/src/text.rs19
-rw-r--r--core/src/text/editor.rs3
-rw-r--r--core/src/text/paragraph.rs1
-rw-r--r--core/src/vector.rs11
-rw-r--r--core/src/widget/operation.rs46
-rw-r--r--core/src/widget/operation/scrollable.rs45
-rw-r--r--core/src/widget/text.rs15
-rw-r--r--core/src/window/settings/linux.rs6
-rw-r--r--core/src/window/settings/windows.rs7
-rw-r--r--examples/component/Cargo.toml10
-rw-r--r--examples/component/src/main.rs149
-rw-r--r--examples/custom_widget/src/main.rs9
-rw-r--r--examples/download_progress/Cargo.toml2
-rw-r--r--examples/download_progress/index.html12
-rw-r--r--examples/download_progress/src/download.rs103
-rw-r--r--examples/download_progress/src/main.rs20
-rw-r--r--examples/editor/src/main.rs44
-rw-r--r--examples/multitouch/src/main.rs2
-rw-r--r--examples/styling/src/main.rs10
-rw-r--r--examples/tour/src/main.rs10
-rw-r--r--graphics/src/cache.rs19
-rw-r--r--graphics/src/geometry/frame.rs24
-rw-r--r--graphics/src/geometry/path.rs13
-rw-r--r--graphics/src/geometry/path/builder.rs68
-rw-r--r--graphics/src/text.rs12
-rw-r--r--graphics/src/text/editor.rs29
-rw-r--r--graphics/src/text/paragraph.rs7
-rw-r--r--renderer/src/fallback.rs13
-rw-r--r--runtime/src/window.rs28
-rw-r--r--tiny_skia/src/geometry.rs9
-rw-r--r--tiny_skia/src/vector.rs31
-rw-r--r--wgpu/src/geometry.rs38
-rw-r--r--wgpu/src/image/vector.rs30
-rw-r--r--wgpu/src/lib.rs1
-rw-r--r--wgpu/src/shader/quad.wgsl14
-rw-r--r--widget/src/checkbox.rs12
-rw-r--r--widget/src/combo_box.rs27
-rw-r--r--widget/src/container.rs1
-rw-r--r--widget/src/helpers.rs4
-rw-r--r--widget/src/lazy.rs2
-rw-r--r--widget/src/lazy/component.rs20
-rw-r--r--widget/src/lazy/helpers.rs12
-rw-r--r--widget/src/lazy/responsive.rs1
-rw-r--r--widget/src/lib.rs3
-rw-r--r--widget/src/mouse_area.rs49
-rw-r--r--widget/src/overlay/menu.rs1
-rw-r--r--widget/src/pick_list.rs5
-rw-r--r--widget/src/progress_bar.rs8
-rw-r--r--widget/src/radio.rs13
-rw-r--r--widget/src/scrollable.rs211
-rw-r--r--widget/src/slider.rs36
-rw-r--r--widget/src/text/rich.rs14
-rw-r--r--widget/src/text_editor.rs26
-rw-r--r--widget/src/text_input.rs96
-rw-r--r--widget/src/toggler.rs82
-rw-r--r--widget/src/vertical_slider.rs12
-rw-r--r--winit/src/clipboard.rs10
-rw-r--r--winit/src/conversion.rs297
-rw-r--r--winit/src/program.rs68
-rw-r--r--winit/src/program/window_manager.rs4
67 files changed, 2028 insertions, 546 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ba1ab003..e7af3b03 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -19,7 +19,7 @@ jobs:
- name: Build todos binary
run: cargo build --verbose --profile release-opt --package todos
- name: Archive todos binary
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: todos-x86_64-unknown-linux-gnu
path: target/release-opt/todos
@@ -28,7 +28,7 @@ jobs:
- name: Rename todos .deb package
run: mv target/debian/*.deb target/debian/iced_todos-x86_64-debian-linux-gnu.deb
- name: Archive todos .deb package
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: todos-x86_64-debian-linux-gnu
path: target/debian/iced_todos-x86_64-debian-linux-gnu.deb
@@ -48,7 +48,7 @@ jobs:
- name: Build todos binary
run: cargo build --verbose --profile release-opt --package todos
- name: Archive todos binary
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: todos-x86_64-pc-windows-msvc
path: target/release-opt/todos.exe
@@ -65,7 +65,7 @@ jobs:
- name: Open binary via double-click
run: chmod +x target/release-opt/todos
- name: Archive todos binary
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: todos-x86_64-apple-darwin
path: target/release-opt/todos
@@ -80,14 +80,14 @@ jobs:
- name: Build todos binary for Raspberry Pi 3/4 (64 bits)
run: cross build --verbose --profile release-opt --package todos --target aarch64-unknown-linux-gnu
- name: Archive todos binary
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: todos-aarch64-unknown-linux-gnu
path: target/aarch64-unknown-linux-gnu/release-opt/todos
- name: Build todos binary for Raspberry Pi 2/3/4 (32 bits)
run: cross build --verbose --profile release-opt --package todos --target armv7-unknown-linux-gnueabihf
- name: Archive todos binary
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: todos-armv7-unknown-linux-gnueabihf
path: target/armv7-unknown-linux-gnueabihf/release-opt/todos
diff --git a/core/src/color.rs b/core/src/color.rs
index 4e79defb..46fe9ecd 100644
--- a/core/src/color.rs
+++ b/core/src/color.rs
@@ -108,6 +108,53 @@ impl Color {
}
}
+ /// Parses a [`Color`] from a hex string.
+ ///
+ /// Supported formats are `#rrggbb`, `#rrggbbaa`, `#rgb`, and `#rgba`.
+ /// The starting "#" is optional. Both uppercase and lowercase are supported.
+ ///
+ /// If you have a static color string, using the [`color!`] macro should be preferred
+ /// since it leverages hexadecimal literal notation and arithmetic directly.
+ ///
+ /// [`color!`]: crate::color!
+ pub fn parse(s: &str) -> Option<Color> {
+ let hex = s.strip_prefix('#').unwrap_or(s);
+
+ let parse_channel = |from: usize, to: usize| {
+ let num =
+ usize::from_str_radix(&hex[from..=to], 16).ok()? as f32 / 255.0;
+
+ // If we only got half a byte (one letter), expand it into a full byte (two letters)
+ Some(if from == to { num + num * 16.0 } else { num })
+ };
+
+ Some(match hex.len() {
+ 3 => Color::from_rgb(
+ parse_channel(0, 0)?,
+ parse_channel(1, 1)?,
+ parse_channel(2, 2)?,
+ ),
+ 4 => Color::from_rgba(
+ parse_channel(0, 0)?,
+ parse_channel(1, 1)?,
+ parse_channel(2, 2)?,
+ parse_channel(3, 3)?,
+ ),
+ 6 => Color::from_rgb(
+ parse_channel(0, 1)?,
+ parse_channel(2, 3)?,
+ parse_channel(4, 5)?,
+ ),
+ 8 => Color::from_rgba(
+ parse_channel(0, 1)?,
+ parse_channel(2, 3)?,
+ parse_channel(4, 5)?,
+ parse_channel(6, 7)?,
+ ),
+ _ => None?,
+ })
+ }
+
/// Converts the [`Color`] into its RGBA8 equivalent.
#[must_use]
pub fn into_rgba8(self) -> [u8; 4] {
@@ -178,34 +225,65 @@ impl From<[f32; 4]> for Color {
///
/// ```
/// # use iced_core::{Color, color};
-/// assert_eq!(color!(0, 0, 0), Color::from_rgb(0., 0., 0.));
-/// assert_eq!(color!(0, 0, 0, 0.), Color::from_rgba(0., 0., 0., 0.));
-/// assert_eq!(color!(0xffffff), Color::from_rgb(1., 1., 1.));
-/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1., 1., 1., 0.));
+/// assert_eq!(color!(0, 0, 0), Color::BLACK);
+/// assert_eq!(color!(0, 0, 0, 0.0), Color::TRANSPARENT);
+/// assert_eq!(color!(0xffffff), Color::from_rgb(1.0, 1.0, 1.0));
+/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1.0, 1.0, 1.0, 0.0));
+/// assert_eq!(color!(0x123), Color::from_rgba8(0x11, 0x22, 0x33, 1.0));
+/// assert_eq!(color!(0x123), color!(0x112233));
/// ```
#[macro_export]
macro_rules! color {
($r:expr, $g:expr, $b:expr) => {
color!($r, $g, $b, 1.0)
};
- ($r:expr, $g:expr, $b:expr, $a:expr) => {
- $crate::Color {
- r: $r as f32 / 255.0,
- g: $g as f32 / 255.0,
- b: $b as f32 / 255.0,
- a: $a,
+ ($r:expr, $g:expr, $b:expr, $a:expr) => {{
+ let r = $r as f32 / 255.0;
+ let g = $g as f32 / 255.0;
+ let b = $b as f32 / 255.0;
+
+ #[allow(clippy::manual_range_contains)]
+ {
+ debug_assert!(
+ r >= 0.0 && r <= 1.0,
+ "R channel must be in [0, 255] range."
+ );
+ debug_assert!(
+ g >= 0.0 && g <= 1.0,
+ "G channel must be in [0, 255] range."
+ );
+ debug_assert!(
+ b >= 0.0 && b <= 1.0,
+ "B channel must be in [0, 255] range."
+ );
}
- };
+
+ $crate::Color { r, g, b, a: $a }
+ }};
($hex:expr) => {{
color!($hex, 1.0)
}};
($hex:expr, $a:expr) => {{
let hex = $hex as u32;
- let r = (hex & 0xff0000) >> 16;
- let g = (hex & 0xff00) >> 8;
- let b = (hex & 0xff);
- color!(r, g, b, $a)
+ if hex <= 0xfff {
+ let r = (hex & 0xf00) >> 8;
+ let g = (hex & 0x0f0) >> 4;
+ let b = (hex & 0x00f);
+
+ color!((r << 4 | r), (g << 4 | g), (b << 4 | b), $a)
+ } else {
+ debug_assert!(
+ hex <= 0xffffff,
+ "color! value must not exceed 0xffffff"
+ );
+
+ let r = (hex & 0xff0000) >> 16;
+ let g = (hex & 0xff00) >> 8;
+ let b = (hex & 0xff);
+
+ color!(r, g, b, $a)
+ }
}};
}
@@ -273,4 +351,23 @@ mod tests {
assert_relative_eq!(result.b, 0.3);
assert_relative_eq!(result.a, 1.0);
}
+
+ #[test]
+ fn parse() {
+ let tests = [
+ ("#ff0000", [255, 0, 0, 255]),
+ ("00ff0080", [0, 255, 0, 128]),
+ ("#F80", [255, 136, 0, 255]),
+ ("#00f1", [0, 0, 255, 17]),
+ ];
+
+ for (arg, expected) in tests {
+ assert_eq!(
+ Color::parse(arg).expect("color must parse").into_rgba8(),
+ expected
+ );
+ }
+
+ assert!(Color::parse("invalid").is_none());
+ }
}
diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs
index 1eb42334..26c45717 100644
--- a/core/src/keyboard/event.rs
+++ b/core/src/keyboard/event.rs
@@ -1,3 +1,4 @@
+use crate::keyboard::key;
use crate::keyboard::{Key, Location, Modifiers};
use crate::SmolStr;
@@ -14,6 +15,12 @@ pub enum Event {
/// The key pressed.
key: Key,
+ /// The key pressed with all keyboard modifiers applied, except Ctrl.
+ modified_key: Key,
+
+ /// The physical key pressed.
+ physical_key: key::Physical,
+
/// The location of the key.
location: Location,
diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs
index dbde5196..479d999b 100644
--- a/core/src/keyboard/key.rs
+++ b/core/src/keyboard/key.rs
@@ -742,3 +742,536 @@ pub enum Named {
/// General-purpose function key.
F35,
}
+
+/// Code representing the location of a physical key.
+///
+/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.code`] with a few
+/// exceptions:
+/// - The keys that the specification calls "MetaLeft" and "MetaRight" are named "SuperLeft" and
+/// "SuperRight" here.
+/// - The key that the specification calls "Super" is reported as `Unidentified` here.
+///
+/// [`KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[allow(missing_docs)]
+#[non_exhaustive]
+pub enum Code {
+ /// <kbd>`</kbd> on a US keyboard. This is also called a backtick or grave.
+ /// This is the <kbd>半角</kbd>/<kbd>全角</kbd>/<kbd>漢字</kbd>
+ /// (hankaku/zenkaku/kanji) key on Japanese keyboards
+ Backquote,
+ /// Used for both the US <kbd>\\</kbd> (on the 101-key layout) and also for the key
+ /// located between the <kbd>"</kbd> and <kbd>Enter</kbd> keys on row C of the 102-,
+ /// 104- and 106-key layouts.
+ /// Labeled <kbd>#</kbd> on a UK (102) keyboard.
+ Backslash,
+ /// <kbd>[</kbd> on a US keyboard.
+ BracketLeft,
+ /// <kbd>]</kbd> on a US keyboard.
+ BracketRight,
+ /// <kbd>,</kbd> on a US keyboard.
+ Comma,
+ /// <kbd>0</kbd> on a US keyboard.
+ Digit0,
+ /// <kbd>1</kbd> on a US keyboard.
+ Digit1,
+ /// <kbd>2</kbd> on a US keyboard.
+ Digit2,
+ /// <kbd>3</kbd> on a US keyboard.
+ Digit3,
+ /// <kbd>4</kbd> on a US keyboard.
+ Digit4,
+ /// <kbd>5</kbd> on a US keyboard.
+ Digit5,
+ /// <kbd>6</kbd> on a US keyboard.
+ Digit6,
+ /// <kbd>7</kbd> on a US keyboard.
+ Digit7,
+ /// <kbd>8</kbd> on a US keyboard.
+ Digit8,
+ /// <kbd>9</kbd> on a US keyboard.
+ Digit9,
+ /// <kbd>=</kbd> on a US keyboard.
+ Equal,
+ /// Located between the left <kbd>Shift</kbd> and <kbd>Z</kbd> keys.
+ /// Labeled <kbd>\\</kbd> on a UK keyboard.
+ IntlBackslash,
+ /// Located between the <kbd>/</kbd> and right <kbd>Shift</kbd> keys.
+ /// Labeled <kbd>\\</kbd> (ro) on a Japanese keyboard.
+ IntlRo,
+ /// Located between the <kbd>=</kbd> and <kbd>Backspace</kbd> keys.
+ /// Labeled <kbd>¥</kbd> (yen) on a Japanese keyboard. <kbd>\\</kbd> on a
+ /// Russian keyboard.
+ IntlYen,
+ /// <kbd>a</kbd> on a US keyboard.
+ /// Labeled <kbd>q</kbd> on an AZERTY (e.g., French) keyboard.
+ KeyA,
+ /// <kbd>b</kbd> on a US keyboard.
+ KeyB,
+ /// <kbd>c</kbd> on a US keyboard.
+ KeyC,
+ /// <kbd>d</kbd> on a US keyboard.
+ KeyD,
+ /// <kbd>e</kbd> on a US keyboard.
+ KeyE,
+ /// <kbd>f</kbd> on a US keyboard.
+ KeyF,
+ /// <kbd>g</kbd> on a US keyboard.
+ KeyG,
+ /// <kbd>h</kbd> on a US keyboard.
+ KeyH,
+ /// <kbd>i</kbd> on a US keyboard.
+ KeyI,
+ /// <kbd>j</kbd> on a US keyboard.
+ KeyJ,
+ /// <kbd>k</kbd> on a US keyboard.
+ KeyK,
+ /// <kbd>l</kbd> on a US keyboard.
+ KeyL,
+ /// <kbd>m</kbd> on a US keyboard.
+ KeyM,
+ /// <kbd>n</kbd> on a US keyboard.
+ KeyN,
+ /// <kbd>o</kbd> on a US keyboard.
+ KeyO,
+ /// <kbd>p</kbd> on a US keyboard.
+ KeyP,
+ /// <kbd>q</kbd> on a US keyboard.
+ /// Labeled <kbd>a</kbd> on an AZERTY (e.g., French) keyboard.
+ KeyQ,
+ /// <kbd>r</kbd> on a US keyboard.
+ KeyR,
+ /// <kbd>s</kbd> on a US keyboard.
+ KeyS,
+ /// <kbd>t</kbd> on a US keyboard.
+ KeyT,
+ /// <kbd>u</kbd> on a US keyboard.
+ KeyU,
+ /// <kbd>v</kbd> on a US keyboard.
+ KeyV,
+ /// <kbd>w</kbd> on a US keyboard.
+ /// Labeled <kbd>z</kbd> on an AZERTY (e.g., French) keyboard.
+ KeyW,
+ /// <kbd>x</kbd> on a US keyboard.
+ KeyX,
+ /// <kbd>y</kbd> on a US keyboard.
+ /// Labeled <kbd>z</kbd> on a QWERTZ (e.g., German) keyboard.
+ KeyY,
+ /// <kbd>z</kbd> on a US keyboard.
+ /// Labeled <kbd>w</kbd> on an AZERTY (e.g., French) keyboard, and <kbd>y</kbd> on a
+ /// QWERTZ (e.g., German) keyboard.
+ KeyZ,
+ /// <kbd>-</kbd> on a US keyboard.
+ Minus,
+ /// <kbd>.</kbd> on a US keyboard.
+ Period,
+ /// <kbd>'</kbd> on a US keyboard.
+ Quote,
+ /// <kbd>;</kbd> on a US keyboard.
+ Semicolon,
+ /// <kbd>/</kbd> on a US keyboard.
+ Slash,
+ /// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
+ AltLeft,
+ /// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
+ /// This is labeled <kbd>AltGr</kbd> on many keyboard layouts.
+ AltRight,
+ /// <kbd>Backspace</kbd> or <kbd>⌫</kbd>.
+ /// Labeled <kbd>Delete</kbd> on Apple keyboards.
+ Backspace,
+ /// <kbd>CapsLock</kbd> or <kbd>⇪</kbd>
+ CapsLock,
+ /// The application context menu key, which is typically found between the right
+ /// <kbd>Super</kbd> key and the right <kbd>Control</kbd> key.
+ ContextMenu,
+ /// <kbd>Control</kbd> or <kbd>⌃</kbd>
+ ControlLeft,
+ /// <kbd>Control</kbd> or <kbd>⌃</kbd>
+ ControlRight,
+ /// <kbd>Enter</kbd> or <kbd>↵</kbd>. Labeled <kbd>Return</kbd> on Apple keyboards.
+ Enter,
+ /// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
+ SuperLeft,
+ /// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
+ SuperRight,
+ /// <kbd>Shift</kbd> or <kbd>⇧</kbd>
+ ShiftLeft,
+ /// <kbd>Shift</kbd> or <kbd>⇧</kbd>
+ ShiftRight,
+ /// <kbd> </kbd> (space)
+ Space,
+ /// <kbd>Tab</kbd> or <kbd>⇥</kbd>
+ Tab,
+ /// Japanese: <kbd>変</kbd> (henkan)
+ Convert,
+ /// Japanese: <kbd>カタカナ</kbd>/<kbd>ひらがな</kbd>/<kbd>ローマ字</kbd>
+ /// (katakana/hiragana/romaji)
+ KanaMode,
+ /// Korean: HangulMode <kbd>한/영</kbd> (han/yeong)
+ ///
+ /// Japanese (Mac keyboard): <kbd>か</kbd> (kana)
+ Lang1,
+ /// Korean: Hanja <kbd>한</kbd> (hanja)
+ ///
+ /// Japanese (Mac keyboard): <kbd>英</kbd> (eisu)
+ Lang2,
+ /// Japanese (word-processing keyboard): Katakana
+ Lang3,
+ /// Japanese (word-processing keyboard): Hiragana
+ Lang4,
+ /// Japanese (word-processing keyboard): Zenkaku/Hankaku
+ Lang5,
+ /// Japanese: <kbd>無変換</kbd> (muhenkan)
+ NonConvert,
+ /// <kbd>⌦</kbd>. The forward delete key.
+ /// Note that on Apple keyboards, the key labelled <kbd>Delete</kbd> on the main part of
+ /// the keyboard is encoded as [`Backspace`].
+ ///
+ /// [`Backspace`]: Self::Backspace
+ Delete,
+ /// <kbd>Page Down</kbd>, <kbd>End</kbd>, or <kbd>↘</kbd>
+ End,
+ /// <kbd>Help</kbd>. Not present on standard PC keyboards.
+ Help,
+ /// <kbd>Home</kbd> or <kbd>↖</kbd>
+ Home,
+ /// <kbd>Insert</kbd> or <kbd>Ins</kbd>. Not present on Apple keyboards.
+ Insert,
+ /// <kbd>Page Down</kbd>, <kbd>PgDn</kbd>, or <kbd>⇟</kbd>
+ PageDown,
+ /// <kbd>Page Up</kbd>, <kbd>PgUp</kbd>, or <kbd>⇞</kbd>
+ PageUp,
+ /// <kbd>↓</kbd>
+ ArrowDown,
+ /// <kbd>←</kbd>
+ ArrowLeft,
+ /// <kbd>→</kbd>
+ ArrowRight,
+ /// <kbd>↑</kbd>
+ ArrowUp,
+ /// On the Mac, this is used for the numpad <kbd>Clear</kbd> key.
+ NumLock,
+ /// <kbd>0 Ins</kbd> on a keyboard. <kbd>0</kbd> on a phone or remote control
+ Numpad0,
+ /// <kbd>1 End</kbd> on a keyboard. <kbd>1</kbd> or <kbd>1 QZ</kbd> on a phone or remote
+ /// control
+ Numpad1,
+ /// <kbd>2 ↓</kbd> on a keyboard. <kbd>2 ABC</kbd> on a phone or remote control
+ Numpad2,
+ /// <kbd>3 PgDn</kbd> on a keyboard. <kbd>3 DEF</kbd> on a phone or remote control
+ Numpad3,
+ /// <kbd>4 ←</kbd> on a keyboard. <kbd>4 GHI</kbd> on a phone or remote control
+ Numpad4,
+ /// <kbd>5</kbd> on a keyboard. <kbd>5 JKL</kbd> on a phone or remote control
+ Numpad5,
+ /// <kbd>6 →</kbd> on a keyboard. <kbd>6 MNO</kbd> on a phone or remote control
+ Numpad6,
+ /// <kbd>7 Home</kbd> on a keyboard. <kbd>7 PQRS</kbd> or <kbd>7 PRS</kbd> on a phone
+ /// or remote control
+ Numpad7,
+ /// <kbd>8 ↑</kbd> on a keyboard. <kbd>8 TUV</kbd> on a phone or remote control
+ Numpad8,
+ /// <kbd>9 PgUp</kbd> on a keyboard. <kbd>9 WXYZ</kbd> or <kbd>9 WXY</kbd> on a phone
+ /// or remote control
+ Numpad9,
+ /// <kbd>+</kbd>
+ NumpadAdd,
+ /// Found on the Microsoft Natural Keyboard.
+ NumpadBackspace,
+ /// <kbd>C</kbd> or <kbd>A</kbd> (All Clear). Also for use with numpads that have a
+ /// <kbd>Clear</kbd> key that is separate from the <kbd>NumLock</kbd> key. On the Mac, the
+ /// numpad <kbd>Clear</kbd> key is encoded as [`NumLock`].
+ ///
+ /// [`NumLock`]: Self::NumLock
+ NumpadClear,
+ /// <kbd>C</kbd> (Clear Entry)
+ NumpadClearEntry,
+ /// <kbd>,</kbd> (thousands separator). For locales where the thousands separator
+ /// is a "." (e.g., Brazil), this key may generate a <kbd>.</kbd>.
+ NumpadComma,
+ /// <kbd>. Del</kbd>. For locales where the decimal separator is "," (e.g.,
+ /// Brazil), this key may generate a <kbd>,</kbd>.
+ NumpadDecimal,
+ /// <kbd>/</kbd>
+ NumpadDivide,
+ NumpadEnter,
+ /// <kbd>=</kbd>
+ NumpadEqual,
+ /// <kbd>#</kbd> on a phone or remote control device. This key is typically found
+ /// below the <kbd>9</kbd> key and to the right of the <kbd>0</kbd> key.
+ NumpadHash,
+ /// <kbd>M</kbd> Add current entry to the value stored in memory.
+ NumpadMemoryAdd,
+ /// <kbd>M</kbd> Clear the value stored in memory.
+ NumpadMemoryClear,
+ /// <kbd>M</kbd> Replace the current entry with the value stored in memory.
+ NumpadMemoryRecall,
+ /// <kbd>M</kbd> Replace the value stored in memory with the current entry.
+ NumpadMemoryStore,
+ /// <kbd>M</kbd> Subtract current entry from the value stored in memory.
+ NumpadMemorySubtract,
+ /// <kbd>*</kbd> on a keyboard. For use with numpads that provide mathematical
+ /// operations (<kbd>+</kbd>, <kbd>-</kbd> <kbd>*</kbd> and <kbd>/</kbd>).
+ ///
+ /// Use `NumpadStar` for the <kbd>*</kbd> key on phones and remote controls.
+ NumpadMultiply,
+ /// <kbd>(</kbd> Found on the Microsoft Natural Keyboard.
+ NumpadParenLeft,
+ /// <kbd>)</kbd> Found on the Microsoft Natural Keyboard.
+ NumpadParenRight,
+ /// <kbd>*</kbd> on a phone or remote control device.
+ ///
+ /// This key is typically found below the <kbd>7</kbd> key and to the left of
+ /// the <kbd>0</kbd> key.
+ ///
+ /// Use <kbd>"NumpadMultiply"</kbd> for the <kbd>*</kbd> key on
+ /// numeric keypads.
+ NumpadStar,
+ /// <kbd>-</kbd>
+ NumpadSubtract,
+ /// <kbd>Esc</kbd> or <kbd>⎋</kbd>
+ Escape,
+ /// <kbd>Fn</kbd> This is typically a hardware key that does not generate a separate code.
+ Fn,
+ /// <kbd>FLock</kbd> or <kbd>FnLock</kbd>. Function Lock key. Found on the Microsoft
+ /// Natural Keyboard.
+ FnLock,
+ /// <kbd>PrtScr SysRq</kbd> or <kbd>Print Screen</kbd>
+ PrintScreen,
+ /// <kbd>Scroll Lock</kbd>
+ ScrollLock,
+ /// <kbd>Pause Break</kbd>
+ Pause,
+ /// Some laptops place this key to the left of the <kbd>↑</kbd> key.
+ ///
+ /// This also the "back" button (triangle) on Android.
+ BrowserBack,
+ BrowserFavorites,
+ /// Some laptops place this key to the right of the <kbd>↑</kbd> key.
+ BrowserForward,
+ /// The "home" button on Android.
+ BrowserHome,
+ BrowserRefresh,
+ BrowserSearch,
+ BrowserStop,
+ /// <kbd>Eject</kbd> or <kbd>⏏</kbd>. This key is placed in the function section on some Apple
+ /// keyboards.
+ Eject,
+ /// Sometimes labelled <kbd>My Computer</kbd> on the keyboard
+ LaunchApp1,
+ /// Sometimes labelled <kbd>Calculator</kbd> on the keyboard
+ LaunchApp2,
+ LaunchMail,
+ MediaPlayPause,
+ MediaSelect,
+ MediaStop,
+ MediaTrackNext,
+ MediaTrackPrevious,
+ /// This key is placed in the function section on some Apple keyboards, replacing the
+ /// <kbd>Eject</kbd> key.
+ Power,
+ Sleep,
+ AudioVolumeDown,
+ AudioVolumeMute,
+ AudioVolumeUp,
+ WakeUp,
+ // Legacy modifier key. Also called "Super" in certain places.
+ Meta,
+ // Legacy modifier key.
+ Hyper,
+ Turbo,
+ Abort,
+ Resume,
+ Suspend,
+ /// Found on Sun’s USB keyboard.
+ Again,
+ /// Found on Sun’s USB keyboard.
+ Copy,
+ /// Found on Sun’s USB keyboard.
+ Cut,
+ /// Found on Sun’s USB keyboard.
+ Find,
+ /// Found on Sun’s USB keyboard.
+ Open,
+ /// Found on Sun’s USB keyboard.
+ Paste,
+ /// Found on Sun’s USB keyboard.
+ Props,
+ /// Found on Sun’s USB keyboard.
+ Select,
+ /// Found on Sun’s USB keyboard.
+ Undo,
+ /// Use for dedicated <kbd>ひらがな</kbd> key found on some Japanese word processing keyboards.
+ Hiragana,
+ /// Use for dedicated <kbd>カタカナ</kbd> key found on some Japanese word processing keyboards.
+ Katakana,
+ /// 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,
+}
+
+/// Contains the platform-native physical key identifier.
+///
+/// The exact values vary from platform to platform (which is part of why this is a per-platform
+/// enum), but the values are primarily tied to the key's physical location on the keyboard.
+///
+/// This enum is primarily used to store raw keycodes when Winit doesn't map a given native
+/// physical key identifier to a meaningful [`Code`] variant. In the presence of identifiers we
+/// haven't mapped for you yet, this lets you use use [`Code`] to:
+///
+/// - Correctly match key press and release events.
+/// - On non-web platforms, support assigning keybinds to virtually any key through a UI.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum NativeCode {
+ /// An unidentified code.
+ Unidentified,
+ /// An Android "scancode".
+ Android(u32),
+ /// A macOS "scancode".
+ MacOS(u16),
+ /// A Windows "scancode".
+ Windows(u16),
+ /// An XKB "keycode".
+ Xkb(u32),
+}
+
+/// Represents the location of a physical key.
+///
+/// This type is a superset of [`Code`], including an [`Unidentified`][Self::Unidentified]
+/// variant.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum Physical {
+ /// A known key code
+ Code(Code),
+ /// This variant is used when the key cannot be translated to a [`Code`]
+ ///
+ /// The native keycode is provided (if available) so you're able to more reliably match
+ /// key-press and key-release events by hashing the [`Physical`] key. It is also possible to use
+ /// this for keybinds for non-standard keys, but such keybinds are tied to a given platform.
+ Unidentified(NativeCode),
+}
+
+impl PartialEq<Code> for Physical {
+ #[inline]
+ fn eq(&self, rhs: &Code) -> bool {
+ match self {
+ Physical::Code(ref code) => code == rhs,
+ Physical::Unidentified(_) => false,
+ }
+ }
+}
+
+impl PartialEq<Physical> for Code {
+ #[inline]
+ fn eq(&self, rhs: &Physical) -> bool {
+ rhs == self
+ }
+}
+
+impl PartialEq<NativeCode> for Physical {
+ #[inline]
+ fn eq(&self, rhs: &NativeCode) -> bool {
+ match self {
+ Physical::Unidentified(ref code) => code == rhs,
+ Physical::Code(_) => false,
+ }
+ }
+}
+
+impl PartialEq<Physical> for NativeCode {
+ #[inline]
+ fn eq(&self, rhs: &Physical) -> bool {
+ rhs == self
+ }
+}
diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs
index 6f3844be..07a4db5a 100644
--- a/core/src/mouse/click.rs
+++ b/core/src/mouse/click.rs
@@ -1,4 +1,5 @@
//! Track mouse clicks.
+use crate::mouse::Button;
use crate::time::Instant;
use crate::Point;
@@ -6,6 +7,7 @@ use crate::Point;
#[derive(Debug, Clone, Copy)]
pub struct Click {
kind: Kind,
+ button: Button,
position: Point,
time: Instant,
}
@@ -36,11 +38,17 @@ impl Kind {
impl Click {
/// Creates a new [`Click`] with the given position and previous last
/// [`Click`].
- pub fn new(position: Point, previous: Option<Click>) -> Click {
+ pub fn new(
+ position: Point,
+ button: Button,
+ previous: Option<Click>,
+ ) -> Click {
let time = Instant::now();
let kind = if let Some(previous) = previous {
- if previous.is_consecutive(position, time) {
+ if previous.is_consecutive(position, time)
+ && button == previous.button
+ {
previous.kind.next()
} else {
Kind::Single
@@ -51,6 +59,7 @@ impl Click {
Click {
kind,
+ button,
position,
time,
}
diff --git a/core/src/mouse/interaction.rs b/core/src/mouse/interaction.rs
index 065eb8e7..aad6a3ea 100644
--- a/core/src/mouse/interaction.rs
+++ b/core/src/mouse/interaction.rs
@@ -13,6 +13,13 @@ pub enum Interaction {
Grabbing,
ResizingHorizontally,
ResizingVertically,
+ ResizingDiagonallyUp,
+ ResizingDiagonallyDown,
NotAllowed,
ZoomIn,
+ ZoomOut,
+ Cell,
+ Move,
+ Copy,
+ Help,
}
diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs
index e3a07280..bbcdd8ff 100644
--- a/core/src/renderer/null.rs
+++ b/core/src/renderer/null.rs
@@ -161,6 +161,7 @@ impl text::Editor for () {
_new_font: Self::Font,
_new_size: Pixels,
_new_line_height: text::LineHeight,
+ _new_wrapping: text::Wrapping,
_new_highlighter: &mut impl text::Highlighter,
) {
}
diff --git a/core/src/text.rs b/core/src/text.rs
index dc8f5785..d7b7fee4 100644
--- a/core/src/text.rs
+++ b/core/src/text.rs
@@ -41,6 +41,9 @@ pub struct Text<Content = String, Font = crate::Font> {
/// The [`Shaping`] strategy of the [`Text`].
pub shaping: Shaping,
+
+ /// The [`Wrapping`] strategy of the [`Text`].
+ pub wrapping: Wrapping,
}
/// The shaping strategy of some text.
@@ -67,6 +70,22 @@ pub enum Shaping {
Advanced,
}
+/// The wrapping strategy of some text.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
+pub enum Wrapping {
+ /// No wrapping.
+ None,
+ /// Wraps at the word level.
+ ///
+ /// This is the default.
+ #[default]
+ Word,
+ /// Wraps at the glyph level.
+ Glyph,
+ /// Wraps at the word level, or fallback to glyph level if a word can't fit on a line by itself.
+ WordOrGlyph,
+}
+
/// The height of a line of text in a paragraph.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LineHeight {
diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs
index 135707d1..cd30db3a 100644
--- a/core/src/text/editor.rs
+++ b/core/src/text/editor.rs
@@ -1,6 +1,6 @@
//! Edit text.
use crate::text::highlighter::{self, Highlighter};
-use crate::text::LineHeight;
+use crate::text::{LineHeight, Wrapping};
use crate::{Pixels, Point, Rectangle, Size};
use std::sync::Arc;
@@ -50,6 +50,7 @@ pub trait Editor: Sized + Default {
new_font: Self::Font,
new_size: Pixels,
new_line_height: LineHeight,
+ new_wrapping: Wrapping,
new_highlighter: &mut impl Highlighter,
);
diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs
index 04a97f35..924276c3 100644
--- a/core/src/text/paragraph.rs
+++ b/core/src/text/paragraph.rs
@@ -95,6 +95,7 @@ impl<P: Paragraph> Plain<P> {
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
+ wrapping: text.wrapping,
}) {
Difference::None => {}
Difference::Bounds => {
diff --git a/core/src/vector.rs b/core/src/vector.rs
index 1380c3b3..ff848c4f 100644
--- a/core/src/vector.rs
+++ b/core/src/vector.rs
@@ -20,6 +20,17 @@ impl Vector {
pub const ZERO: Self = Self::new(0.0, 0.0);
}
+impl<T> std::ops::Neg for Vector<T>
+where
+ T: std::ops::Neg<Output = T>,
+{
+ type Output = Self;
+
+ fn neg(self) -> Self::Output {
+ Self::new(-self.x, -self.y)
+ }
+}
+
impl<T> std::ops::Add for Vector<T>
where
T: std::ops::Add<Output = T>,
diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs
index 4ee4b4a7..097c3601 100644
--- a/core/src/widget/operation.rs
+++ b/core/src/widget/operation.rs
@@ -38,6 +38,7 @@ pub trait Operation<T = ()>: Send {
_state: &mut dyn Scrollable,
_id: Option<&Id>,
_bounds: Rectangle,
+ _content_bounds: Rectangle,
_translation: Vector,
) {
}
@@ -76,9 +77,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
+ content_bounds: Rectangle,
translation: Vector,
) {
- self.as_mut().scrollable(state, id, bounds, translation);
+ self.as_mut().scrollable(
+ state,
+ id,
+ bounds,
+ content_bounds,
+ translation,
+ );
}
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
@@ -151,9 +159,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
+ content_bounds: Rectangle,
translation: Vector,
) {
- self.operation.scrollable(state, id, bounds, translation);
+ self.operation.scrollable(
+ state,
+ id,
+ bounds,
+ content_bounds,
+ translation,
+ );
}
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
@@ -222,9 +237,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
+ content_bounds: Rectangle,
translation: Vector,
) {
- self.operation.scrollable(state, id, bounds, translation);
+ self.operation.scrollable(
+ state,
+ id,
+ bounds,
+ content_bounds,
+ translation,
+ );
}
fn focusable(
@@ -262,9 +284,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
+ content_bounds: Rectangle,
translation: Vector,
) {
- self.operation.scrollable(state, id, bounds, translation);
+ self.operation.scrollable(
+ state,
+ id,
+ bounds,
+ content_bounds,
+ translation,
+ );
}
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
@@ -341,9 +370,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
+ content_bounds: Rectangle,
translation: crate::Vector,
) {
- self.operation.scrollable(state, id, bounds, translation);
+ self.operation.scrollable(
+ state,
+ id,
+ bounds,
+ content_bounds,
+ translation,
+ );
}
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs
index 12161255..c2fecf56 100644
--- a/core/src/widget/operation/scrollable.rs
+++ b/core/src/widget/operation/scrollable.rs
@@ -9,6 +9,14 @@ pub trait Scrollable {
/// Scroll the widget to the given [`AbsoluteOffset`] along the horizontal & vertical axis.
fn scroll_to(&mut self, offset: AbsoluteOffset);
+
+ /// Scroll the widget by the given [`AbsoluteOffset`] along the horizontal & vertical axis.
+ fn scroll_by(
+ &mut self,
+ offset: AbsoluteOffset,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ );
}
/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to
@@ -34,6 +42,7 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
state: &mut dyn Scrollable,
id: Option<&Id>,
_bounds: Rectangle,
+ _content_bounds: Rectangle,
_translation: Vector,
) {
if Some(&self.target) == id {
@@ -68,6 +77,7 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
state: &mut dyn Scrollable,
id: Option<&Id>,
_bounds: Rectangle,
+ _content_bounds: Rectangle,
_translation: Vector,
) {
if Some(&self.target) == id {
@@ -79,6 +89,41 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
ScrollTo { target, offset }
}
+/// Produces an [`Operation`] that scrolls the widget with the given [`Id`] by
+/// the provided [`AbsoluteOffset`].
+pub fn scroll_by<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
+ struct ScrollBy {
+ target: Id,
+ offset: AbsoluteOffset,
+ }
+
+ impl<T> Operation<T> for ScrollBy {
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ _bounds: Rectangle,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ ) {
+ operate_on_children(self);
+ }
+
+ fn scrollable(
+ &mut self,
+ state: &mut dyn Scrollable,
+ id: Option<&Id>,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ _translation: Vector,
+ ) {
+ if Some(&self.target) == id {
+ state.scroll_by(self.offset, bounds, content_bounds);
+ }
+ }
+ }
+
+ ScrollBy { target, offset }
+}
+
/// The amount of absolute offset in each direction of a [`Scrollable`].
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct AbsoluteOffset {
diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs
index 5c5b78dd..d8d6e4c6 100644
--- a/core/src/widget/text.rs
+++ b/core/src/widget/text.rs
@@ -11,7 +11,7 @@ use crate::{
Widget,
};
-pub use text::{LineHeight, Shaping};
+pub use text::{LineHeight, Shaping, Wrapping};
/// A paragraph of text.
#[allow(missing_debug_implementations)]
@@ -29,6 +29,7 @@ where
vertical_alignment: alignment::Vertical,
font: Option<Renderer::Font>,
shaping: Shaping,
+ wrapping: Wrapping,
class: Theme::Class<'a>,
}
@@ -48,7 +49,8 @@ where
height: Length::Shrink,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
- shaping: Shaping::Basic,
+ shaping: Shaping::default(),
+ wrapping: Wrapping::default(),
class: Theme::default(),
}
}
@@ -115,6 +117,12 @@ where
self
}
+ /// Sets the [`Wrapping`] strategy of the [`Text`].
+ pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
+ self.wrapping = wrapping;
+ self
+ }
+
/// Sets the style of the [`Text`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
@@ -198,6 +206,7 @@ where
self.horizontal_alignment,
self.vertical_alignment,
self.shaping,
+ self.wrapping,
)
}
@@ -232,6 +241,7 @@ pub fn layout<Renderer>(
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
shaping: Shaping,
+ wrapping: Wrapping,
) -> layout::Node
where
Renderer: text::Renderer,
@@ -253,6 +263,7 @@ where
horizontal_alignment,
vertical_alignment,
shaping,
+ wrapping,
});
paragraph.min_bounds()
diff --git a/core/src/window/settings/linux.rs b/core/src/window/settings/linux.rs
index 009b9d9e..0a1e11cd 100644
--- a/core/src/window/settings/linux.rs
+++ b/core/src/window/settings/linux.rs
@@ -8,4 +8,10 @@ pub struct PlatformSpecific {
/// As a best practice, it is suggested to select an application id that match
/// the basename of the application’s .desktop file.
pub application_id: String,
+
+ /// Whether bypass the window manager mapping for x11 windows
+ ///
+ /// This flag is particularly useful for creating UI elements that need precise
+ /// positioning and immediate display without window manager interference.
+ pub override_redirect: bool,
}
diff --git a/core/src/window/settings/windows.rs b/core/src/window/settings/windows.rs
index 88fe2fbd..a47582a6 100644
--- a/core/src/window/settings/windows.rs
+++ b/core/src/window/settings/windows.rs
@@ -8,6 +8,12 @@ pub struct PlatformSpecific {
/// Whether show or hide the window icon in the taskbar.
pub skip_taskbar: bool,
+
+ /// Shows or hides the background drop shadow for undecorated windows.
+ ///
+ /// The shadow is hidden by default.
+ /// Enabling the shadow causes a thin 1px line to appear on the top of the window.
+ pub undecorated_shadow: bool,
}
impl Default for PlatformSpecific {
@@ -15,6 +21,7 @@ impl Default for PlatformSpecific {
Self {
drag_and_drop: true,
skip_taskbar: false,
+ undecorated_shadow: false,
}
}
}
diff --git a/examples/component/Cargo.toml b/examples/component/Cargo.toml
deleted file mode 100644
index 83b7b8a4..00000000
--- a/examples/component/Cargo.toml
+++ /dev/null
@@ -1,10 +0,0 @@
-[package]
-name = "component"
-version = "0.1.0"
-authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
-edition = "2021"
-publish = false
-
-[dependencies]
-iced.workspace = true
-iced.features = ["debug", "lazy"]
diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs
deleted file mode 100644
index a5d2e508..00000000
--- a/examples/component/src/main.rs
+++ /dev/null
@@ -1,149 +0,0 @@
-use iced::widget::center;
-use iced::Element;
-
-use numeric_input::numeric_input;
-
-pub fn main() -> iced::Result {
- iced::run("Component - Iced", Component::update, Component::view)
-}
-
-#[derive(Default)]
-struct Component {
- value: Option<u32>,
-}
-
-#[derive(Debug, Clone, Copy)]
-enum Message {
- NumericInputChanged(Option<u32>),
-}
-
-impl Component {
- fn update(&mut self, message: Message) {
- match message {
- Message::NumericInputChanged(value) => {
- self.value = value;
- }
- }
- }
-
- fn view(&self) -> Element<Message> {
- center(numeric_input(self.value, Message::NumericInputChanged))
- .padding(20)
- .into()
- }
-}
-
-mod numeric_input {
- use iced::widget::{button, component, row, text, text_input, Component};
- use iced::{Center, Element, Fill, Length, Size};
-
- pub struct NumericInput<Message> {
- value: Option<u32>,
- on_change: Box<dyn Fn(Option<u32>) -> Message>,
- }
-
- pub fn numeric_input<Message>(
- value: Option<u32>,
- on_change: impl Fn(Option<u32>) -> Message + 'static,
- ) -> NumericInput<Message> {
- NumericInput::new(value, on_change)
- }
-
- #[derive(Debug, Clone)]
- pub enum Event {
- InputChanged(String),
- IncrementPressed,
- DecrementPressed,
- }
-
- impl<Message> NumericInput<Message> {
- pub fn new(
- value: Option<u32>,
- on_change: impl Fn(Option<u32>) -> Message + 'static,
- ) -> Self {
- Self {
- value,
- on_change: Box::new(on_change),
- }
- }
- }
-
- impl<Message, Theme> Component<Message, Theme> for NumericInput<Message>
- where
- Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static,
- {
- type State = ();
- type Event = Event;
-
- fn update(
- &mut self,
- _state: &mut Self::State,
- event: Event,
- ) -> Option<Message> {
- match event {
- Event::IncrementPressed => Some((self.on_change)(Some(
- self.value.unwrap_or_default().saturating_add(1),
- ))),
- Event::DecrementPressed => Some((self.on_change)(Some(
- self.value.unwrap_or_default().saturating_sub(1),
- ))),
- Event::InputChanged(value) => {
- if value.is_empty() {
- Some((self.on_change)(None))
- } else {
- value
- .parse()
- .ok()
- .map(Some)
- .map(self.on_change.as_ref())
- }
- }
- }
- }
-
- fn view(&self, _state: &Self::State) -> Element<'_, Event, Theme> {
- let button = |label, on_press| {
- button(text(label).width(Fill).height(Fill).center())
- .width(40)
- .height(40)
- .on_press(on_press)
- };
-
- row![
- button("-", Event::DecrementPressed),
- text_input(
- "Type a number",
- self.value
- .as_ref()
- .map(u32::to_string)
- .as_deref()
- .unwrap_or(""),
- )
- .on_input(Event::InputChanged)
- .padding(10),
- button("+", Event::IncrementPressed),
- ]
- .align_y(Center)
- .spacing(10)
- .into()
- }
-
- fn size_hint(&self) -> Size<Length> {
- Size {
- width: Length::Fill,
- height: Length::Shrink,
- }
- }
- }
-
- impl<'a, Message, Theme> From<NumericInput<Message>>
- for Element<'a, Message, Theme>
- where
- Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static,
- Message: 'a,
- {
- fn from(numeric_input: NumericInput<Message>) -> Self {
- component(numeric_input)
- }
- }
-}
diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs
index dc3f74ac..58f3c54a 100644
--- a/examples/custom_widget/src/main.rs
+++ b/examples/custom_widget/src/main.rs
@@ -1,14 +1,5 @@
//! This example showcases a simple native custom widget that draws a circle.
mod circle {
- // For now, to implement a custom native widget you will need to add
- // `iced_native` and `iced_wgpu` to your dependencies.
- //
- // Then, you simply need to define your widget type and implement the
- // `iced_native::Widget` trait with the `iced_wgpu::Renderer`.
- //
- // Of course, you can choose to make the implementation renderer-agnostic,
- // if you wish to, by creating your own `Renderer` trait, which could be
- // implemented by `iced_wgpu` and other renderers.
use iced::advanced::layout::{self, Layout};
use iced::advanced::renderer;
use iced::advanced::widget::{self, Widget};
diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml
index f78df529..61a1b257 100644
--- a/examples/download_progress/Cargo.toml
+++ b/examples/download_progress/Cargo.toml
@@ -12,4 +12,4 @@ iced.features = ["tokio"]
[dependencies.reqwest]
version = "0.12"
default-features = false
-features = ["rustls-tls"]
+features = ["stream", "rustls-tls"]
diff --git a/examples/download_progress/index.html b/examples/download_progress/index.html
new file mode 100644
index 00000000..c79e32c1
--- /dev/null
+++ b/examples/download_progress/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en" style="height: 100%">
+<head>
+ <meta charset="utf-8" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>Download_Progress - Iced</title>
+ <base data-trunk-public-url />
+</head>
+<body style="height: 100%; margin: 0">
+<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="z" data-bin="download_progress" />
+</body>
+</html>
diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs
index bdf57290..a8e7b404 100644
--- a/examples/download_progress/src/download.rs
+++ b/examples/download_progress/src/download.rs
@@ -1,91 +1,62 @@
-use iced::futures;
+use iced::futures::{SinkExt, Stream, StreamExt};
+use iced::stream::try_channel;
use iced::Subscription;
use std::hash::Hash;
+use std::sync::Arc;
// Just a little utility function
pub fn file<I: 'static + Hash + Copy + Send + Sync, T: ToString>(
id: I,
url: T,
-) -> iced::Subscription<(I, Progress)> {
+) -> iced::Subscription<(I, Result<Progress, Error>)> {
Subscription::run_with_id(
id,
- futures::stream::unfold(State::Ready(url.to_string()), move |state| {
- use iced::futures::FutureExt;
-
- download(id, state).map(Some)
- }),
+ download(url.to_string()).map(move |progress| (id, progress)),
)
}
-async fn download<I: Copy>(id: I, state: State) -> ((I, Progress), State) {
- match state {
- State::Ready(url) => {
- let response = reqwest::get(&url).await;
+fn download(url: String) -> impl Stream<Item = Result<Progress, Error>> {
+ try_channel(1, move |mut output| async move {
+ let response = reqwest::get(&url).await?;
+ let total = response.content_length().ok_or(Error::NoContentLength)?;
- match response {
- Ok(response) => {
- if let Some(total) = response.content_length() {
- (
- (id, Progress::Started),
- State::Downloading {
- response,
- total,
- downloaded: 0,
- },
- )
- } else {
- ((id, Progress::Errored), State::Finished)
- }
- }
- Err(_) => ((id, Progress::Errored), State::Finished),
- }
- }
- State::Downloading {
- mut response,
- total,
- downloaded,
- } => match response.chunk().await {
- Ok(Some(chunk)) => {
- let downloaded = downloaded + chunk.len() as u64;
+ let _ = output.send(Progress::Downloading { percent: 0.0 }).await;
+
+ let mut byte_stream = response.bytes_stream();
+ let mut downloaded = 0;
- let percentage = (downloaded as f32 / total as f32) * 100.0;
+ while let Some(next_bytes) = byte_stream.next().await {
+ let bytes = next_bytes?;
+ downloaded += bytes.len();
- (
- (id, Progress::Advanced(percentage)),
- State::Downloading {
- response,
- total,
- downloaded,
- },
- )
- }
- Ok(None) => ((id, Progress::Finished), State::Finished),
- Err(_) => ((id, Progress::Errored), State::Finished),
- },
- State::Finished => {
- // We do not let the stream die, as it would start a
- // new download repeatedly if the user is not careful
- // in case of errors.
- iced::futures::future::pending().await
+ let _ = output
+ .send(Progress::Downloading {
+ percent: 100.0 * downloaded as f32 / total as f32,
+ })
+ .await;
}
- }
+
+ let _ = output.send(Progress::Finished).await;
+
+ Ok(())
+ })
}
#[derive(Debug, Clone)]
pub enum Progress {
- Started,
- Advanced(f32),
+ Downloading { percent: f32 },
Finished,
- Errored,
}
-pub enum State {
- Ready(String),
- Downloading {
- response: reqwest::Response,
- total: u64,
- downloaded: u64,
- },
- Finished,
+#[derive(Debug, Clone)]
+pub enum Error {
+ RequestFailed(Arc<reqwest::Error>),
+ NoContentLength,
+}
+
+impl From<reqwest::Error> for Error {
+ fn from(error: reqwest::Error) -> Self {
+ Error::RequestFailed(Arc::new(error))
+ }
}
diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs
index 667fb448..bcc01606 100644
--- a/examples/download_progress/src/main.rs
+++ b/examples/download_progress/src/main.rs
@@ -23,7 +23,7 @@ struct Example {
pub enum Message {
Add,
Download(usize),
- DownloadProgressed((usize, download::Progress)),
+ DownloadProgressed((usize, Result<download::Progress, download::Error>)),
}
impl Example {
@@ -114,19 +114,19 @@ impl Download {
}
}
- pub fn progress(&mut self, new_progress: download::Progress) {
+ pub fn progress(
+ &mut self,
+ new_progress: Result<download::Progress, download::Error>,
+ ) {
if let State::Downloading { progress } = &mut self.state {
match new_progress {
- download::Progress::Started => {
- *progress = 0.0;
+ Ok(download::Progress::Downloading { percent }) => {
+ *progress = percent;
}
- download::Progress::Advanced(percentage) => {
- *progress = percentage;
- }
- download::Progress::Finished => {
+ Ok(download::Progress::Finished) => {
self.state = State::Finished;
}
- download::Progress::Errored => {
+ Err(_error) => {
self.state = State::Errored;
}
}
@@ -136,7 +136,7 @@ impl Download {
pub fn subscription(&self) -> Subscription<Message> {
match self.state {
State::Downloading { .. } => {
- download::file(self.id, "https://speed.hetzner.de/100MB.bin?")
+ download::file(self.id, "https://huggingface.co/mattshumer/Reflection-Llama-3.1-70B/resolve/main/model-00001-of-00162.safetensors")
.map(Message::DownloadProgressed)
}
_ => Subscription::none(),
diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs
index aa07b328..d55f9bdf 100644
--- a/examples/editor/src/main.rs
+++ b/examples/editor/src/main.rs
@@ -2,9 +2,9 @@ use iced::highlighter;
use iced::keyboard;
use iced::widget::{
self, button, column, container, horizontal_space, pick_list, row, text,
- text_editor, tooltip,
+ text_editor, toggler, tooltip,
};
-use iced::{Center, Element, Fill, Font, Subscription, Task, Theme};
+use iced::{Center, Element, Fill, Font, Task, Theme};
use std::ffi;
use std::io;
@@ -13,7 +13,6 @@ use std::sync::Arc;
pub fn main() -> iced::Result {
iced::application("Editor - Iced", Editor::update, Editor::view)
- .subscription(Editor::subscription)
.theme(Editor::theme)
.font(include_bytes!("../fonts/icons.ttf").as_slice())
.default_font(Font::MONOSPACE)
@@ -24,6 +23,7 @@ struct Editor {
file: Option<PathBuf>,
content: text_editor::Content,
theme: highlighter::Theme,
+ word_wrap: bool,
is_loading: bool,
is_dirty: bool,
}
@@ -32,6 +32,7 @@ struct Editor {
enum Message {
ActionPerformed(text_editor::Action),
ThemeSelected(highlighter::Theme),
+ WordWrapToggled(bool),
NewFile,
OpenFile,
FileOpened(Result<(PathBuf, Arc<String>), Error>),
@@ -46,6 +47,7 @@ impl Editor {
file: None,
content: text_editor::Content::new(),
theme: highlighter::Theme::SolarizedDark,
+ word_wrap: true,
is_loading: true,
is_dirty: false,
},
@@ -76,6 +78,11 @@ impl Editor {
Task::none()
}
+ Message::WordWrapToggled(word_wrap) => {
+ self.word_wrap = word_wrap;
+
+ Task::none()
+ }
Message::NewFile => {
if !self.is_loading {
self.file = None;
@@ -129,15 +136,6 @@ impl Editor {
}
}
- 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)),
@@ -152,6 +150,9 @@ impl Editor {
self.is_dirty.then_some(Message::SaveFile)
),
horizontal_space(),
+ toggler(self.word_wrap)
+ .label("Word Wrap")
+ .on_toggle(Message::WordWrapToggled),
pick_list(
highlighter::Theme::ALL,
Some(self.theme),
@@ -189,6 +190,11 @@ impl Editor {
text_editor(&self.content)
.height(Fill)
.on_action(Message::ActionPerformed)
+ .wrapping(if self.word_wrap {
+ text::Wrapping::Word
+ } else {
+ text::Wrapping::None
+ })
.highlight(
self.file
.as_deref()
@@ -196,7 +202,19 @@ impl Editor {
.and_then(ffi::OsStr::to_str)
.unwrap_or("rs"),
self.theme,
- ),
+ )
+ .key_binding(|key_press| {
+ match key_press.key.as_ref() {
+ keyboard::Key::Character("s")
+ if key_press.modifiers.command() =>
+ {
+ Some(text_editor::Binding::Custom(
+ Message::SaveFile,
+ ))
+ }
+ _ => text_editor::Binding::from_key_press(key_press),
+ }
+ }),
status,
]
.spacing(10)
diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs
index a0105a8a..d5e5dffa 100644
--- a/examples/multitouch/src/main.rs
+++ b/examples/multitouch/src/main.rs
@@ -126,7 +126,7 @@ impl canvas::Program<Message> for Multitouch {
let path = builder.build();
- let color_r = (10 % zone.0) as f32 / 20.0;
+ let color_r = (10 % (zone.0 + 1)) as f32 / 20.0;
let color_g = (10 % (zone.0 + 8)) as f32 / 20.0;
let color_b = (10 % (zone.0 + 3)) as f32 / 20.0;
diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs
index 527aaa29..534f5e32 100644
--- a/examples/styling/src/main.rs
+++ b/examples/styling/src/main.rs
@@ -77,12 +77,10 @@ impl Styling {
let checkbox = checkbox("Check me!", self.checkbox_value)
.on_toggle(Message::CheckboxToggled);
- let toggler = toggler(
- String::from("Toggle me!"),
- self.toggler_value,
- Message::TogglerToggled,
- )
- .spacing(10);
+ let toggler = toggler(self.toggler_value)
+ .label("Toggle me!")
+ .on_toggle(Message::TogglerToggled)
+ .spacing(10);
let content = column![
choose_theme,
diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs
index ee4754e6..d8c0b29a 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -357,11 +357,11 @@ impl Tour {
Self::container("Toggler")
.push("A toggler is mostly used to enable or disable something.")
.push(
- Container::new(toggler(
- "Toggle me to continue...".to_owned(),
- self.toggler,
- Message::TogglerChanged,
- ))
+ Container::new(
+ toggler(self.toggler)
+ .label("Toggle me to continue...")
+ .on_toggle(Message::TogglerChanged),
+ )
.padding([0, 40]),
)
}
diff --git a/graphics/src/cache.rs b/graphics/src/cache.rs
index bbba79eb..7db80a01 100644
--- a/graphics/src/cache.rs
+++ b/graphics/src/cache.rs
@@ -1,6 +1,7 @@
//! Cache computations and efficiently reuse them.
use std::cell::RefCell;
use std::fmt;
+use std::mem;
use std::sync::atomic::{self, AtomicU64};
/// A simple cache that stores generated values to avoid recomputation.
@@ -58,18 +59,18 @@ impl<T> Cache<T> {
}
/// Clears the [`Cache`].
- pub fn clear(&self)
- where
- T: Clone,
- {
- use std::ops::Deref;
+ pub fn clear(&self) {
+ let mut state = self.state.borrow_mut();
+
+ let previous =
+ mem::replace(&mut *state, State::Empty { previous: None });
- let previous = match self.state.borrow().deref() {
- State::Empty { previous } => previous.clone(),
- State::Filled { current } => Some(current.clone()),
+ let previous = match previous {
+ State::Empty { previous } => previous,
+ State::Filled { current } => Some(current),
};
- *self.state.borrow_mut() = State::Empty { previous };
+ *state = State::Empty { previous };
}
}
diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs
index b5f2f139..3dee7e75 100644
--- a/graphics/src/geometry/frame.rs
+++ b/graphics/src/geometry/frame.rs
@@ -65,6 +65,17 @@ where
self.raw.stroke(path, stroke);
}
+ /// Draws the stroke of an axis-aligned rectangle with the provided style
+ /// given its top-left corner coordinate and its `Size` on the [`Frame`] .
+ pub fn stroke_rectangle<'a>(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ stroke: impl Into<Stroke<'a>>,
+ ) {
+ self.raw.stroke_rectangle(top_left, size, stroke);
+ }
+
/// Draws the characters of the given [`Text`] on the [`Frame`], filling
/// them with the given color.
///
@@ -200,6 +211,12 @@ pub trait Backend: Sized {
fn paste(&mut self, frame: Self);
fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>);
+ fn stroke_rectangle<'a>(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ stroke: impl Into<Stroke<'a>>,
+ );
fn fill(&mut self, path: &Path, fill: impl Into<Fill>);
fn fill_text(&mut self, text: impl Into<Text>);
@@ -248,6 +265,13 @@ impl Backend for () {
fn paste(&mut self, _frame: Self) {}
fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into<Stroke<'a>>) {}
+ fn stroke_rectangle<'a>(
+ &mut self,
+ _top_left: Point,
+ _size: Size,
+ _stroke: impl Into<Stroke<'a>>,
+ ) {
+ }
fn fill(&mut self, _path: &Path, _fill: impl Into<Fill>) {}
fn fill_text(&mut self, _text: impl Into<Text>) {}
diff --git a/graphics/src/geometry/path.rs b/graphics/src/geometry/path.rs
index 3d8fc6fa..c4f51593 100644
--- a/graphics/src/geometry/path.rs
+++ b/graphics/src/geometry/path.rs
@@ -9,7 +9,8 @@ pub use builder::Builder;
pub use lyon_path;
-use iced_core::{Point, Size};
+use crate::core::border;
+use crate::core::{Point, Size};
/// An immutable set of points that may or may not be connected.
///
@@ -47,6 +48,16 @@ impl Path {
Self::new(|p| p.rectangle(top_left, size))
}
+ /// Creates a new [`Path`] representing a rounded rectangle given its top-left
+ /// corner coordinate, its [`Size`] and [`border::Radius`].
+ pub fn rounded_rectangle(
+ top_left: Point,
+ size: Size,
+ radius: border::Radius,
+ ) -> Self {
+ Self::new(|p| p.rounded_rectangle(top_left, size, radius))
+ }
+
/// Creates a new [`Path`] representing a circle given its center
/// coordinate and its radius.
pub fn circle(center: Point, radius: f32) -> Self {
diff --git a/graphics/src/geometry/path/builder.rs b/graphics/src/geometry/path/builder.rs
index 1ccd83f2..44410f6d 100644
--- a/graphics/src/geometry/path/builder.rs
+++ b/graphics/src/geometry/path/builder.rs
@@ -1,6 +1,7 @@
use crate::geometry::path::{arc, Arc, Path};
-use iced_core::{Point, Radians, Size};
+use crate::core::border;
+use crate::core::{Point, Radians, Size};
use lyon_path::builder::{self, SvgPathBuilder};
use lyon_path::geom;
@@ -160,6 +161,71 @@ impl Builder {
self.close();
}
+ /// Adds a rounded rectangle to the [`Path`] given its top-left
+ /// corner coordinate its [`Size`] and [`border::Radius`].
+ #[inline]
+ pub fn rounded_rectangle(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ radius: border::Radius,
+ ) {
+ let min_size = (size.height / 2.0).min(size.width / 2.0);
+ let [top_left_corner, top_right_corner, bottom_right_corner, bottom_left_corner] =
+ radius.into();
+
+ self.move_to(Point::new(
+ top_left.x + min_size.min(top_left_corner),
+ top_left.y,
+ ));
+ self.line_to(Point::new(
+ top_left.x + size.width - min_size.min(top_right_corner),
+ top_left.y,
+ ));
+ self.arc_to(
+ Point::new(top_left.x + size.width, top_left.y),
+ Point::new(
+ top_left.x + size.width,
+ top_left.y + min_size.min(top_right_corner),
+ ),
+ min_size.min(top_right_corner),
+ );
+ self.line_to(Point::new(
+ top_left.x + size.width,
+ top_left.y + size.height - min_size.min(bottom_right_corner),
+ ));
+ self.arc_to(
+ Point::new(top_left.x + size.width, top_left.y + size.height),
+ Point::new(
+ top_left.x + size.width - min_size.min(bottom_right_corner),
+ top_left.y + size.height,
+ ),
+ min_size.min(bottom_right_corner),
+ );
+ self.line_to(Point::new(
+ top_left.x + min_size.min(bottom_left_corner),
+ top_left.y + size.height,
+ ));
+ self.arc_to(
+ Point::new(top_left.x, top_left.y + size.height),
+ Point::new(
+ top_left.x,
+ top_left.y + size.height - min_size.min(bottom_left_corner),
+ ),
+ min_size.min(bottom_left_corner),
+ );
+ self.line_to(Point::new(
+ top_left.x,
+ top_left.y + min_size.min(top_left_corner),
+ ));
+ self.arc_to(
+ Point::new(top_left.x, top_left.y),
+ Point::new(top_left.x + min_size.min(top_left_corner), top_left.y),
+ min_size.min(top_left_corner),
+ );
+ self.close();
+ }
+
/// Adds a circle to the [`Path`] given its center coordinate and its
/// radius.
#[inline]
diff --git a/graphics/src/text.rs b/graphics/src/text.rs
index 23ec14d4..feb9932a 100644
--- a/graphics/src/text.rs
+++ b/graphics/src/text.rs
@@ -11,7 +11,7 @@ pub use cosmic_text;
use crate::core::alignment;
use crate::core::font::{self, Font};
-use crate::core::text::Shaping;
+use crate::core::text::{Shaping, Wrapping};
use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation};
use once_cell::sync::OnceCell;
@@ -306,6 +306,16 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
}
}
+/// Converts some [`Wrapping`] strategy to a [`cosmic_text::Wrap`] strategy.
+pub fn to_wrap(wrapping: Wrapping) -> cosmic_text::Wrap {
+ match wrapping {
+ Wrapping::None => cosmic_text::Wrap::None,
+ Wrapping::Word => cosmic_text::Wrap::Word,
+ Wrapping::Glyph => cosmic_text::Wrap::Glyph,
+ Wrapping::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph,
+ }
+}
+
/// Converts some [`Color`] to a [`cosmic_text::Color`].
pub fn to_color(color: Color) -> cosmic_text::Color {
let [r, g, b, a] = color.into_rgba8();
diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs
index 80733bbb..1f1d0050 100644
--- a/graphics/src/text/editor.rs
+++ b/graphics/src/text/editor.rs
@@ -3,7 +3,7 @@ use crate::core::text::editor::{
self, Action, Cursor, Direction, Edit, Motion,
};
use crate::core::text::highlighter::{self, Highlighter};
-use crate::core::text::LineHeight;
+use crate::core::text::{LineHeight, Wrapping};
use crate::core::{Font, Pixels, Point, Rectangle, Size};
use crate::text;
@@ -437,6 +437,7 @@ impl editor::Editor for Editor {
new_font: Font,
new_size: Pixels,
new_line_height: LineHeight,
+ new_wrapping: Wrapping,
new_highlighter: &mut impl Highlighter,
) {
let editor =
@@ -448,13 +449,12 @@ impl editor::Editor for Editor {
let mut font_system =
text::font_system().write().expect("Write font system");
+ let buffer = buffer_mut_from_editor(&mut internal.editor);
+
if font_system.version() != internal.version {
log::trace!("Updating `FontSystem` of `Editor`...");
- for line in buffer_mut_from_editor(&mut internal.editor)
- .lines
- .iter_mut()
- {
+ for line in buffer.lines.iter_mut() {
line.reset();
}
@@ -465,10 +465,7 @@ impl editor::Editor for Editor {
if new_font != internal.font {
log::trace!("Updating font of `Editor`...");
- for line in buffer_mut_from_editor(&mut internal.editor)
- .lines
- .iter_mut()
- {
+ for line in buffer.lines.iter_mut() {
let _ = line.set_attrs_list(cosmic_text::AttrsList::new(
text::to_attributes(new_font),
));
@@ -478,7 +475,7 @@ impl editor::Editor for Editor {
internal.topmost_line_changed = Some(0);
}
- let metrics = buffer_from_editor(&internal.editor).metrics();
+ let metrics = buffer.metrics();
let new_line_height = new_line_height.to_absolute(new_size);
if new_size.0 != metrics.font_size
@@ -486,16 +483,24 @@ impl editor::Editor for Editor {
{
log::trace!("Updating `Metrics` of `Editor`...");
- buffer_mut_from_editor(&mut internal.editor).set_metrics(
+ buffer.set_metrics(
font_system.raw(),
cosmic_text::Metrics::new(new_size.0, new_line_height.0),
);
}
+ let new_wrap = text::to_wrap(new_wrapping);
+
+ if new_wrap != buffer.wrap() {
+ log::trace!("Updating `Wrap` strategy of `Editor`...");
+
+ buffer.set_wrap(font_system.raw(), new_wrap);
+ }
+
if new_bounds != internal.bounds {
log::trace!("Updating size of `Editor`...");
- buffer_mut_from_editor(&mut internal.editor).set_size(
+ buffer.set_size(
font_system.raw(),
Some(new_bounds.width),
Some(new_bounds.height),
diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs
index b9f9c833..07ddbb82 100644
--- a/graphics/src/text/paragraph.rs
+++ b/graphics/src/text/paragraph.rs
@@ -1,7 +1,7 @@
//! Draw paragraphs.
use crate::core;
use crate::core::alignment;
-use crate::core::text::{Hit, Shaping, Span, Text};
+use crate::core::text::{Hit, Shaping, Span, Text, Wrapping};
use crate::core::{Font, Point, Rectangle, Size};
use crate::text;
@@ -17,6 +17,7 @@ struct Internal {
buffer: cosmic_text::Buffer,
font: Font,
shaping: Shaping,
+ wrapping: Wrapping,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
bounds: Size,
@@ -94,6 +95,7 @@ impl core::text::Paragraph for Paragraph {
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
+ wrapping: text.wrapping,
bounds: text.bounds,
min_bounds,
version: font_system.version(),
@@ -160,6 +162,7 @@ impl core::text::Paragraph for Paragraph {
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
+ wrapping: text.wrapping,
bounds: text.bounds,
min_bounds,
version: font_system.version(),
@@ -192,6 +195,7 @@ impl core::text::Paragraph for Paragraph {
|| metrics.line_height != text.line_height.to_absolute(text.size).0
|| paragraph.font != text.font
|| paragraph.shaping != text.shaping
+ || paragraph.wrapping != text.wrapping
|| paragraph.horizontal_alignment != text.horizontal_alignment
|| paragraph.vertical_alignment != text.vertical_alignment
{
@@ -387,6 +391,7 @@ impl Default for Internal {
}),
font: Font::default(),
shaping: Shaping::default(),
+ wrapping: Wrapping::default(),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
bounds: Size::ZERO,
diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs
index fbd285db..8cb18bde 100644
--- a/renderer/src/fallback.rs
+++ b/renderer/src/fallback.rs
@@ -540,6 +540,19 @@ mod geometry {
delegate!(self, frame, frame.stroke(path, stroke));
}
+ fn stroke_rectangle<'a>(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ stroke: impl Into<Stroke<'a>>,
+ ) {
+ delegate!(
+ self,
+ frame,
+ frame.stroke_rectangle(top_left, size, stroke)
+ );
+ }
+
fn fill_text(&mut self, text: impl Into<Text>) {
delegate!(self, frame, frame.fill_text(text));
}
diff --git a/runtime/src/window.rs b/runtime/src/window.rs
index ce6fd1b6..cdf3d80a 100644
--- a/runtime/src/window.rs
+++ b/runtime/src/window.rs
@@ -147,6 +147,18 @@ pub enum Action {
/// Screenshot the viewport of the window.
Screenshot(Id, oneshot::Sender<Screenshot>),
+
+ /// Enables mouse passthrough for the given window.
+ ///
+ /// This disables mouse events for the window and passes mouse events
+ /// through to whatever window is underneath.
+ EnableMousePassthrough(Id),
+
+ /// Disable mouse passthrough for the given window.
+ ///
+ /// This enables mouse events for the window and stops mouse events
+ /// from being passed to whatever is underneath.
+ DisableMousePassthrough(Id),
}
/// Subscribes to the frames of the window of the running application.
@@ -406,3 +418,19 @@ pub fn screenshot(id: Id) -> Task<Screenshot> {
crate::Action::Window(Action::Screenshot(id, channel))
})
}
+
+/// Enables mouse passthrough for the given window.
+///
+/// This disables mouse events for the window and passes mouse events
+/// through to whatever window is underneath.
+pub fn enable_mouse_passthrough<Message>(id: Id) -> Task<Message> {
+ task::effect(crate::Action::Window(Action::EnableMousePassthrough(id)))
+}
+
+/// Disable mouse passthrough for the given window.
+///
+/// This enables mouse events for the window and stops mouse events
+/// from being passed to whatever is underneath.
+pub fn disable_mouse_passthrough<Message>(id: Id) -> Task<Message> {
+ task::effect(crate::Action::Window(Action::DisableMousePassthrough(id)))
+}
diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs
index 659612d1..0d5fff62 100644
--- a/tiny_skia/src/geometry.rs
+++ b/tiny_skia/src/geometry.rs
@@ -168,6 +168,15 @@ impl geometry::frame::Backend for Frame {
});
}
+ fn stroke_rectangle<'a>(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ stroke: impl Into<Stroke<'a>>,
+ ) {
+ self.stroke(&Path::rectangle(top_left, size), stroke);
+ }
+
fn fill_text(&mut self, text: impl Into<geometry::Text>) {
let text = text.into();
diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs
index 8a15f47f..ea7de215 100644
--- a/tiny_skia/src/vector.rs
+++ b/tiny_skia/src/vector.rs
@@ -8,6 +8,7 @@ use tiny_skia::Transform;
use std::cell::RefCell;
use std::collections::hash_map;
use std::fs;
+use std::sync::Arc;
#[derive(Debug)]
pub struct Pipeline {
@@ -68,6 +69,7 @@ struct Cache {
tree_hits: FxHashSet<u64>,
rasters: FxHashMap<RasterKey, tiny_skia::Pixmap>,
raster_hits: FxHashSet<RasterKey>,
+ fontdb: Option<Arc<usvg::fontdb::Database>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -81,23 +83,32 @@ impl Cache {
fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> {
let id = handle.id();
+ // TODO: Reuse `cosmic-text` font database
+ if self.fontdb.is_none() {
+ let mut fontdb = usvg::fontdb::Database::new();
+ fontdb.load_system_fonts();
+
+ self.fontdb = Some(Arc::new(fontdb));
+ }
+
+ let options = usvg::Options {
+ fontdb: self
+ .fontdb
+ .as_ref()
+ .expect("fontdb must be initialized")
+ .clone(),
+ ..usvg::Options::default()
+ };
+
if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) {
let svg = match handle.data() {
Data::Path(path) => {
fs::read_to_string(path).ok().and_then(|contents| {
- usvg::Tree::from_str(
- &contents,
- &usvg::Options::default(), // TODO: Set usvg::Options::fontdb
- )
- .ok()
+ usvg::Tree::from_str(&contents, &options).ok()
})
}
Data::Bytes(bytes) => {
- usvg::Tree::from_data(
- bytes,
- &usvg::Options::default(), // TODO: Set usvg::Options::fontdb
- )
- .ok()
+ usvg::Tree::from_data(bytes, &options).ok()
}
};
diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs
index be65ba36..8e6f77d7 100644
--- a/wgpu/src/geometry.rs
+++ b/wgpu/src/geometry.rs
@@ -253,6 +253,44 @@ impl geometry::frame::Backend for Frame {
.expect("Stroke path");
}
+ fn stroke_rectangle<'a>(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ stroke: impl Into<Stroke<'a>>,
+ ) {
+ let stroke = stroke.into();
+
+ let mut buffer = self
+ .buffers
+ .get_stroke(&self.transforms.current.transform_style(stroke.style));
+
+ let top_left = self
+ .transforms
+ .current
+ .0
+ .transform_point(lyon::math::Point::new(top_left.x, top_left.y));
+
+ let size =
+ self.transforms.current.0.transform_vector(
+ lyon::math::Vector::new(size.width, size.height),
+ );
+
+ let mut options = tessellation::StrokeOptions::default();
+ options.line_width = stroke.width;
+ options.start_cap = into_line_cap(stroke.line_cap);
+ options.end_cap = into_line_cap(stroke.line_cap);
+ options.line_join = into_line_join(stroke.line_join);
+
+ self.stroke_tessellator
+ .tessellate_rectangle(
+ &lyon::math::Box2D::new(top_left, top_left + size),
+ &options,
+ buffer.as_mut(),
+ )
+ .expect("Stroke rectangle");
+ }
+
fn fill_text(&mut self, text: impl Into<geometry::Text>) {
let text = text.into();
diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs
index 74e9924d..e55ade38 100644
--- a/wgpu/src/image/vector.rs
+++ b/wgpu/src/image/vector.rs
@@ -6,6 +6,7 @@ use resvg::tiny_skia;
use resvg::usvg;
use rustc_hash::{FxHashMap, FxHashSet};
use std::fs;
+use std::sync::Arc;
/// Entry in cache corresponding to an svg handle
pub enum Svg {
@@ -37,6 +38,7 @@ pub struct Cache {
svg_hits: FxHashSet<u64>,
rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>,
should_trim: bool,
+ fontdb: Option<Arc<usvg::fontdb::Database>>,
}
type ColorFilter = Option<[u8; 4]>;
@@ -48,23 +50,33 @@ impl Cache {
return self.svgs.get(&handle.id()).unwrap();
}
+ // TODO: Reuse `cosmic-text` font database
+ if self.fontdb.is_none() {
+ let mut fontdb = usvg::fontdb::Database::new();
+ fontdb.load_system_fonts();
+
+ self.fontdb = Some(Arc::new(fontdb));
+ }
+
+ let options = usvg::Options {
+ fontdb: self
+ .fontdb
+ .as_ref()
+ .expect("fontdb must be initialized")
+ .clone(),
+ ..usvg::Options::default()
+ };
+
let svg = match handle.data() {
svg::Data::Path(path) => fs::read_to_string(path)
.ok()
.and_then(|contents| {
- usvg::Tree::from_str(
- &contents,
- &usvg::Options::default(), // TODO: Set usvg::Options::fontdb
- )
- .ok()
+ usvg::Tree::from_str(&contents, &options).ok()
})
.map(Svg::Loaded)
.unwrap_or(Svg::NotFound),
svg::Data::Bytes(bytes) => {
- match usvg::Tree::from_data(
- bytes,
- &usvg::Options::default(), // TODO: Set usvg::Options::fontdb
- ) {
+ match usvg::Tree::from_data(bytes, &options) {
Ok(tree) => Svg::Loaded(tree),
Err(_) => Svg::NotFound,
}
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index 39167514..d79f0dc8 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -408,6 +408,7 @@ impl Renderer {
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: core::text::Shaping::Basic,
+ wrapping: core::text::Wrapping::Word,
};
renderer.fill_text(
diff --git a/wgpu/src/shader/quad.wgsl b/wgpu/src/shader/quad.wgsl
index a367d5e6..b213c8cf 100644
--- a/wgpu/src/shader/quad.wgsl
+++ b/wgpu/src/shader/quad.wgsl
@@ -22,14 +22,14 @@ fn rounded_box_sdf(to_center: vec2<f32>, size: vec2<f32>, radius: f32) -> f32 {
return length(max(abs(to_center) - size + vec2<f32>(radius, radius), vec2<f32>(0.0, 0.0))) - radius;
}
-// Based on the fragment position and the center of the quad, select one of the 4 radi.
+// Based on the fragment position and the center of the quad, select one of the 4 radii.
// Order matches CSS border radius attribute:
-// radi.x = top-left, radi.y = top-right, radi.z = bottom-right, radi.w = bottom-left
-fn select_border_radius(radi: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 {
- var rx = radi.x;
- var ry = radi.y;
- rx = select(radi.x, radi.y, position.x > center.x);
- ry = select(radi.w, radi.z, position.x > center.x);
+// radii.x = top-left, radii.y = top-right, radii.z = bottom-right, radii.w = bottom-left
+fn select_border_radius(radii: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 {
+ var rx = radii.x;
+ var ry = radii.y;
+ rx = select(radii.x, radii.y, position.x > center.x);
+ ry = select(radii.w, radii.z, position.x > center.x);
rx = select(rx, ry, position.y > center.y);
return rx;
}
diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs
index e5abfbb4..32db5090 100644
--- a/widget/src/checkbox.rs
+++ b/widget/src/checkbox.rs
@@ -50,6 +50,7 @@ pub struct Checkbox<
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
+ text_wrapping: text::Wrapping,
font: Option<Renderer::Font>,
icon: Icon<Renderer::Font>,
class: Theme::Class<'a>,
@@ -81,7 +82,8 @@ where
spacing: Self::DEFAULT_SPACING,
text_size: None,
text_line_height: text::LineHeight::default(),
- text_shaping: text::Shaping::Basic,
+ text_shaping: text::Shaping::default(),
+ text_wrapping: text::Wrapping::default(),
font: None,
icon: Icon {
font: Renderer::ICON_FONT,
@@ -158,6 +160,12 @@ where
self
}
+ /// Sets the [`text::Wrapping`] strategy of the [`Checkbox`].
+ pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
+ self.text_wrapping = wrapping;
+ self
+ }
+
/// Sets the [`Renderer::Font`] of the text of the [`Checkbox`].
///
/// [`Renderer::Font`]: crate::core::text::Renderer
@@ -240,6 +248,7 @@ where
alignment::Horizontal::Left,
alignment::Vertical::Top,
self.text_shaping,
+ self.text_wrapping,
)
},
)
@@ -348,6 +357,7 @@ where
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
shaping: *shaping,
+ wrapping: text::Wrapping::default(),
},
bounds.center(),
style.icon_color,
diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs
index 62785b2c..a51701ca 100644
--- a/widget/src/combo_box.rs
+++ b/widget/src/combo_box.rs
@@ -41,6 +41,7 @@ pub struct ComboBox<
selection: text_input::Value,
on_selected: Box<dyn Fn(T) -> Message>,
on_option_hovered: Option<Box<dyn Fn(T) -> Message>>,
+ on_open: Option<Message>,
on_close: Option<Message>,
on_input: Option<Box<dyn Fn(String) -> Message>>,
menu_class: <Theme as menu::Catalog>::Class<'a>,
@@ -77,6 +78,7 @@ where
on_selected: Box::new(on_selected),
on_option_hovered: None,
on_input: None,
+ on_open: None,
on_close: None,
menu_class: <Theme as Catalog>::default_menu(),
padding: text_input::DEFAULT_PADDING,
@@ -104,6 +106,13 @@ where
self
}
+ /// Sets the message that will be produced when the [`ComboBox`] is
+ /// opened.
+ pub fn on_open(mut self, message: Message) -> Self {
+ self.on_open = Some(message);
+ self
+ }
+
/// Sets the message that will be produced when the outside area
/// of the [`ComboBox`] is pressed.
pub fn on_close(mut self, message: Message) -> Self {
@@ -632,15 +641,19 @@ where
text_input_state.is_focused()
};
- if started_focused && !is_focused && !published_message_to_shell {
- if let Some(message) = self.on_close.take() {
- shell.publish(message);
- }
- }
-
- // Focus changed, invalidate widget tree to force a fresh `view`
if started_focused != is_focused {
+ // Focus changed, invalidate widget tree to force a fresh `view`
shell.invalidate_widgets();
+
+ if !published_message_to_shell {
+ if is_focused {
+ if let Some(on_open) = self.on_open.take() {
+ shell.publish(on_open);
+ }
+ } else if let Some(on_close) = self.on_close.take() {
+ shell.publish(on_close);
+ }
+ }
}
event_status
diff --git a/widget/src/container.rs b/widget/src/container.rs
index c3a66360..3b794099 100644
--- a/widget/src/container.rs
+++ b/widget/src/container.rs
@@ -459,6 +459,7 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> {
_state: &mut dyn widget::operation::Scrollable,
_id: Option<&widget::Id>,
bounds: Rectangle,
+ _content_bounds: Rectangle,
translation: Vector,
) {
match self.scrollables.last() {
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index 1cb02830..51978823 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -767,15 +767,13 @@ where
///
/// [`Toggler`]: crate::Toggler
pub fn toggler<'a, Message, Theme, Renderer>(
- label: impl Into<Option<String>>,
is_checked: bool,
- f: impl Fn(bool) -> Message + 'a,
) -> Toggler<'a, Message, Theme, Renderer>
where
Theme: toggler::Catalog + 'a,
Renderer: core::text::Renderer,
{
- Toggler::new(label, is_checked, f)
+ Toggler::new(is_checked)
}
/// Creates a new [`TextInput`].
diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs
index 4bcf8628..221f9de3 100644
--- a/widget/src/lazy.rs
+++ b/widget/src/lazy.rs
@@ -4,6 +4,7 @@ pub(crate) mod helpers;
pub mod component;
pub mod responsive;
+#[allow(deprecated)]
pub use component::Component;
pub use responsive::Responsive;
@@ -29,6 +30,7 @@ use std::hash::{Hash, Hasher as H};
use std::rc::Rc;
/// A widget that only rebuilds its contents when necessary.
+#[cfg(feature = "lazy")]
#[allow(missing_debug_implementations)]
pub struct Lazy<'a, Message, Theme, Renderer, Dependency, View> {
dependency: Dependency,
diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs
index 1bf04195..2bdfa2c0 100644
--- a/widget/src/lazy/component.rs
+++ b/widget/src/lazy/component.rs
@@ -1,4 +1,5 @@
//! Build and reuse custom widgets using The Elm Architecture.
+#![allow(deprecated)]
use crate::core::event;
use crate::core::layout::{self, Layout};
use crate::core::mouse;
@@ -30,6 +31,25 @@ use std::rc::Rc;
///
/// Additionally, a [`Component`] is capable of producing a `Message` to notify
/// the parent application of any relevant interactions.
+///
+/// # State
+/// A component can store its state in one of two ways: either as data within the
+/// implementor of the trait, or in a type [`State`][Component::State] that is managed
+/// by the runtime and provided to the trait methods. These two approaches are not
+/// mutually exclusive and have opposite pros and cons.
+///
+/// For instance, if a piece of state is needed by multiple components that reside
+/// in different branches of the tree, then it's more convenient to let a common
+/// ancestor store it and pass it down.
+///
+/// On the other hand, if a piece of state is only needed by the component itself,
+/// you can store it as part of its internal [`State`][Component::State].
+#[cfg(feature = "lazy")]
+#[deprecated(
+ since = "0.13.0",
+ note = "components introduce encapsulated state and hamper the use of a single source of truth. \
+ Instead, leverage the Elm Architecture directly, or implement a custom widget"
+)]
pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> {
/// The internal state of this [`Component`].
type State: Default;
diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs
index 4d0776ca..52e690ff 100644
--- a/widget/src/lazy/helpers.rs
+++ b/widget/src/lazy/helpers.rs
@@ -1,9 +1,11 @@
use crate::core::{self, Element, Size};
-use crate::lazy::component::{self, Component};
-use crate::lazy::{Lazy, Responsive};
+use crate::lazy::component;
use std::hash::Hash;
+#[allow(deprecated)]
+pub use crate::lazy::{Component, Lazy, Responsive};
+
/// Creates a new [`Lazy`] widget with the given data `Dependency` and a
/// closure that can turn this data into a widget tree.
#[cfg(feature = "lazy")]
@@ -21,6 +23,12 @@ where
/// Turns an implementor of [`Component`] into an [`Element`] that can be
/// embedded in any application.
#[cfg(feature = "lazy")]
+#[deprecated(
+ since = "0.13.0",
+ note = "components introduce encapsulated state and hamper the use of a single source of truth. \
+ Instead, leverage the Elm Architecture directly, or implement a custom widget"
+)]
+#[allow(deprecated)]
pub fn component<'a, C, Message, Theme, Renderer>(
component: C,
) -> Element<'a, Message, Theme, Renderer>
diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs
index 2e24f2b3..dbf281f3 100644
--- a/widget/src/lazy/responsive.rs
+++ b/widget/src/lazy/responsive.rs
@@ -21,6 +21,7 @@ use std::ops::Deref;
///
/// A [`Responsive`] widget will always try to fill all the available space of
/// its parent.
+#[cfg(feature = "lazy")]
#[allow(missing_debug_implementations)]
pub struct Responsive<
'a,
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
index 115a29e5..a68720d6 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -43,9 +43,6 @@ pub use helpers::*;
mod lazy;
#[cfg(feature = "lazy")]
-pub use crate::lazy::{Component, Lazy, Responsive};
-
-#[cfg(feature = "lazy")]
pub use crate::lazy::helpers::*;
#[doc(no_inline)]
diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs
index 366335f4..d255ac99 100644
--- a/widget/src/mouse_area.rs
+++ b/widget/src/mouse_area.rs
@@ -1,7 +1,4 @@
//! A container for capturing mouse events.
-
-use iced_renderer::core::Point;
-
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
@@ -10,7 +7,8 @@ use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::{tree, Operation, Tree};
use crate::core::{
- Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget,
+ Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector,
+ Widget,
};
/// Emit messages on mouse events.
@@ -28,8 +26,9 @@ pub struct MouseArea<
on_right_release: Option<Message>,
on_middle_press: Option<Message>,
on_middle_release: Option<Message>,
+ on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta) -> Message + 'a>>,
on_enter: Option<Message>,
- on_move: Option<Box<dyn Fn(Point) -> Message>>,
+ on_move: Option<Box<dyn Fn(Point) -> Message + 'a>>,
on_exit: Option<Message>,
interaction: Option<mouse::Interaction>,
}
@@ -77,6 +76,16 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
self
}
+ /// The message to emit when scroll wheel is used
+ #[must_use]
+ pub fn on_scroll(
+ mut self,
+ on_scroll: impl Fn(mouse::ScrollDelta) -> Message + 'a,
+ ) -> Self {
+ self.on_scroll = Some(Box::new(on_scroll));
+ self
+ }
+
/// The message to emit when the mouse enters the area.
#[must_use]
pub fn on_enter(mut self, message: Message) -> Self {
@@ -86,11 +95,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
/// The message to emit when the mouse moves in the area.
#[must_use]
- pub fn on_move<F>(mut self, build_message: F) -> Self
- where
- F: Fn(Point) -> Message + 'static,
- {
- self.on_move = Some(Box::new(build_message));
+ pub fn on_move(mut self, on_move: impl Fn(Point) -> Message + 'a) -> Self {
+ self.on_move = Some(Box::new(on_move));
self
}
@@ -113,6 +119,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
#[derive(Default)]
struct State {
is_hovered: bool,
+ bounds: Rectangle,
+ cursor_position: Option<Point>,
}
impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
@@ -128,6 +136,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
on_right_release: None,
on_middle_press: None,
on_middle_release: None,
+ on_scroll: None,
on_enter: None,
on_move: None,
on_exit: None,
@@ -302,13 +311,17 @@ fn update<Message: Clone, Theme, Renderer>(
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
) -> event::Status {
- if let Event::Mouse(mouse::Event::CursorMoved { .. })
- | Event::Touch(touch::Event::FingerMoved { .. }) = event
- {
- let state: &mut State = tree.state.downcast_mut();
+ let state: &mut State = tree.state.downcast_mut();
+ let cursor_position = cursor.position();
+ let bounds = layout.bounds();
+
+ if state.cursor_position != cursor_position && state.bounds != bounds {
let was_hovered = state.is_hovered;
+
state.is_hovered = cursor.is_over(layout.bounds());
+ state.cursor_position = cursor_position;
+ state.bounds = bounds;
match (
widget.on_enter.as_ref(),
@@ -397,5 +410,13 @@ fn update<Message: Clone, Theme, Renderer>(
}
}
+ if let Some(on_scroll) = widget.on_scroll.as_ref() {
+ if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
+ shell.publish(on_scroll(delta));
+
+ return event::Status::Captured;
+ }
+ }
+
event::Status::Ignored
}
diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs
index 73d1cc8c..f05ae40a 100644
--- a/widget/src/overlay/menu.rs
+++ b/widget/src/overlay/menu.rs
@@ -532,6 +532,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
+ wrapping: text::Wrapping::default(),
},
Point::new(bounds.x + self.padding.left, bounds.center_y()),
if is_selected {
diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs
index f7f7b65b..1fc9951e 100644
--- a/widget/src/pick_list.rs
+++ b/widget/src/pick_list.rs
@@ -81,7 +81,7 @@ where
padding: crate::button::DEFAULT_PADDING,
text_size: None,
text_line_height: text::LineHeight::default(),
- text_shaping: text::Shaping::Basic,
+ text_shaping: text::Shaping::default(),
font: None,
handle: Handle::default(),
class: <Theme as Catalog>::default(),
@@ -250,6 +250,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
+ wrapping: text::Wrapping::default(),
};
for (option, paragraph) in options.iter().zip(state.options.iter_mut())
@@ -515,6 +516,7 @@ where
horizontal_alignment: alignment::Horizontal::Right,
vertical_alignment: alignment::Vertical::Center,
shaping,
+ wrapping: text::Wrapping::default(),
},
Point::new(
bounds.x + bounds.width - self.padding.right,
@@ -544,6 +546,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
+ wrapping: text::Wrapping::default(),
},
Point::new(bounds.x + self.padding.left, bounds.center_y()),
if is_selected {
diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs
index 88d1850a..a10feea6 100644
--- a/widget/src/progress_bar.rs
+++ b/widget/src/progress_bar.rs
@@ -5,7 +5,8 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{
- self, Background, Element, Layout, Length, Rectangle, Size, Theme, Widget,
+ self, Background, Color, Element, Layout, Length, Rectangle, Size, Theme,
+ Widget,
};
use std::ops::RangeInclusive;
@@ -151,7 +152,10 @@ where
width: active_progress_width,
..bounds
},
- border: border::rounded(style.border.radius),
+ border: Border {
+ color: Color::TRANSPARENT,
+ ..style.border
+ },
..renderer::Quad::default()
},
style.bar,
diff --git a/widget/src/radio.rs b/widget/src/radio.rs
index 1b02f8ca..cfa961f3 100644
--- a/widget/src/radio.rs
+++ b/widget/src/radio.rs
@@ -82,6 +82,7 @@ where
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
+ text_wrapping: text::Wrapping,
font: Option<Renderer::Font>,
class: Theme::Class<'a>,
}
@@ -122,10 +123,11 @@ where
label: label.into(),
width: Length::Shrink,
size: Self::DEFAULT_SIZE,
- spacing: Self::DEFAULT_SPACING, //15
+ spacing: Self::DEFAULT_SPACING,
text_size: None,
text_line_height: text::LineHeight::default(),
- text_shaping: text::Shaping::Basic,
+ text_shaping: text::Shaping::default(),
+ text_wrapping: text::Wrapping::default(),
font: None,
class: Theme::default(),
}
@@ -170,6 +172,12 @@ where
self
}
+ /// Sets the [`text::Wrapping`] strategy of the [`Radio`] button.
+ pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
+ self.text_wrapping = wrapping;
+ self
+ }
+
/// Sets the text font of the [`Radio`] button.
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
@@ -245,6 +253,7 @@ where
alignment::Horizontal::Left,
alignment::Vertical::Top,
self.text_shaping,
+ self.text_wrapping,
)
},
)
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index cf504eda..af6a3945 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -7,10 +7,12 @@ use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
+use crate::core::time::{Duration, Instant};
use crate::core::touch;
use crate::core::widget;
use crate::core::widget::operation::{self, Operation};
use crate::core::widget::tree::{self, Tree};
+use crate::core::window;
use crate::core::{
self, Background, Clipboard, Color, Element, Layout, Length, Padding,
Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
@@ -242,6 +244,24 @@ impl Direction {
Self::Horizontal(_) => None,
}
}
+
+ fn align(&self, delta: Vector) -> Vector {
+ let horizontal_alignment =
+ self.horizontal().map(|p| p.alignment).unwrap_or_default();
+
+ let vertical_alignment =
+ self.vertical().map(|p| p.alignment).unwrap_or_default();
+
+ let align = |alignment: Anchor, delta: f32| match alignment {
+ Anchor::Start => delta,
+ Anchor::End => -delta,
+ };
+
+ Vector::new(
+ align(horizontal_alignment, delta.x),
+ align(vertical_alignment, delta.y),
+ )
+ }
}
impl Default for Direction {
@@ -429,6 +449,7 @@ where
state,
self.id.as_ref().map(|id| &id.0),
bounds,
+ content_bounds,
translation,
);
@@ -470,6 +491,24 @@ where
let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
scrollbars.is_mouse_over(cursor);
+ if let Some(last_scrolled) = state.last_scrolled {
+ let clear_transaction = match event {
+ Event::Mouse(
+ mouse::Event::ButtonPressed(_)
+ | mouse::Event::ButtonReleased(_)
+ | mouse::Event::CursorLeft,
+ ) => true,
+ Event::Mouse(mouse::Event::CursorMoved { .. }) => {
+ last_scrolled.elapsed() > Duration::from_millis(100)
+ }
+ _ => last_scrolled.elapsed() > Duration::from_millis(1500),
+ };
+
+ if clear_transaction {
+ state.last_scrolled = None;
+ }
+ }
+
if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
match event {
Event::Mouse(mouse::Event::CursorMoved { .. })
@@ -488,7 +527,7 @@ where
content_bounds,
);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -526,7 +565,7 @@ where
state.y_scroller_grabbed_at = Some(scroller_grabbed_at);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -559,7 +598,7 @@ where
content_bounds,
);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -597,7 +636,7 @@ where
state.x_scroller_grabbed_at = Some(scroller_grabbed_at);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -612,7 +651,11 @@ where
}
}
- let mut event_status = {
+ let content_status = if state.last_scrolled.is_some()
+ && matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. }))
+ {
+ event::Status::Ignored
+ } else {
let cursor = match cursor_over_scrollable {
Some(cursor_position)
if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
@@ -660,10 +703,10 @@ where
state.x_scroller_grabbed_at = None;
state.y_scroller_grabbed_at = None;
- return event_status;
+ return content_status;
}
- if let event::Status::Captured = event_status {
+ if let event::Status::Captured = content_status {
return event::Status::Captured;
}
@@ -683,23 +726,41 @@ where
let delta = match delta {
mouse::ScrollDelta::Lines { x, y } => {
- // TODO: Configurable speed/friction (?)
- let movement = if !cfg!(target_os = "macos") // macOS automatically inverts the axes when Shift is pressed
- && state.keyboard_modifiers.shift()
- {
- Vector::new(y, x)
- } else {
+ let is_shift_pressed = state.keyboard_modifiers.shift();
+
+ // macOS automatically inverts the axes when Shift is pressed
+ let (x, y) =
+ if cfg!(target_os = "macos") && is_shift_pressed {
+ (y, x)
+ } else {
+ (x, y)
+ };
+
+ let is_vertical = match self.direction {
+ Direction::Vertical(_) => true,
+ Direction::Horizontal(_) => false,
+ Direction::Both { .. } => !is_shift_pressed,
+ };
+
+ let movement = if is_vertical {
Vector::new(x, y)
+ } else {
+ Vector::new(y, x)
};
- movement * 60.0
+ // TODO: Configurable speed/friction (?)
+ -movement * 60.0
}
mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y),
};
- state.scroll(delta, self.direction, bounds, content_bounds);
+ state.scroll(
+ self.direction.align(delta),
+ bounds,
+ content_bounds,
+ );
- event_status = if notify_on_scroll(
+ if notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -709,7 +770,7 @@ where
event::Status::Captured
} else {
event::Status::Ignored
- };
+ }
}
Event::Touch(event)
if state.scroll_area_touched_at.is_some()
@@ -733,13 +794,12 @@ where
};
let delta = Vector::new(
- cursor_position.x - scroll_box_touched_at.x,
- cursor_position.y - scroll_box_touched_at.y,
+ scroll_box_touched_at.x - cursor_position.x,
+ scroll_box_touched_at.y - cursor_position.y,
);
state.scroll(
- delta,
- self.direction,
+ self.direction.align(delta),
bounds,
content_bounds,
);
@@ -748,7 +808,7 @@ where
Some(cursor_position);
// TODO: bubble up touch movements if not consumed.
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -760,12 +820,21 @@ where
_ => {}
}
- event_status = event::Status::Captured;
+ event::Status::Captured
}
- _ => {}
- }
+ Event::Window(window::Event::RedrawRequested(_)) => {
+ let _ = notify_viewport(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
- event_status
+ event::Status::Ignored
+ }
+ _ => event::Status::Ignored,
+ }
}
fn draw(
@@ -1075,21 +1144,44 @@ impl From<Id> for widget::Id {
}
/// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`]
-/// to the provided `percentage` along the x & y axis.
+/// to the provided [`RelativeOffset`].
pub fn snap_to<T>(id: Id, offset: RelativeOffset) -> Task<T> {
task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset)))
}
/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`]
-/// to the provided [`AbsoluteOffset`] along the x & y axis.
+/// to the provided [`AbsoluteOffset`].
pub fn scroll_to<T>(id: Id, offset: AbsoluteOffset) -> Task<T> {
task::effect(Action::widget(operation::scrollable::scroll_to(
id.0, offset,
)))
}
-/// Returns [`true`] if the viewport actually changed.
-fn notify_on_scroll<Message>(
+/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`]
+/// by the provided [`AbsoluteOffset`].
+pub fn scroll_by<T>(id: Id, offset: AbsoluteOffset) -> Task<T> {
+ task::effect(Action::widget(operation::scrollable::scroll_by(
+ id.0, offset,
+ )))
+}
+
+fn notify_scroll<Message>(
+ state: &mut State,
+ on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ shell: &mut Shell<'_, Message>,
+) -> bool {
+ if notify_viewport(state, on_scroll, bounds, content_bounds, shell) {
+ state.last_scrolled = Some(Instant::now());
+
+ true
+ } else {
+ false
+ }
+}
+
+fn notify_viewport<Message>(
state: &mut State,
on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
bounds: Rectangle,
@@ -1102,6 +1194,11 @@ fn notify_on_scroll<Message>(
return false;
}
+ let Some(on_scroll) = on_scroll else {
+ state.last_notified = None;
+ return false;
+ };
+
let viewport = Viewport {
offset_x: state.offset_x,
offset_y: state.offset_y,
@@ -1121,7 +1218,9 @@ fn notify_on_scroll<Message>(
(a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
};
- if unchanged(last_relative_offset.x, current_relative_offset.x)
+ if last_notified.bounds == bounds
+ && last_notified.content_bounds == content_bounds
+ && unchanged(last_relative_offset.x, current_relative_offset.x)
&& unchanged(last_relative_offset.y, current_relative_offset.y)
&& unchanged(last_absolute_offset.x, current_absolute_offset.x)
&& unchanged(last_absolute_offset.y, current_absolute_offset.y)
@@ -1130,9 +1229,7 @@ fn notify_on_scroll<Message>(
}
}
- if let Some(on_scroll) = on_scroll {
- shell.publish(on_scroll(viewport));
- }
+ shell.publish(on_scroll(viewport));
state.last_notified = Some(viewport);
true
@@ -1147,6 +1244,7 @@ struct State {
x_scroller_grabbed_at: Option<f32>,
keyboard_modifiers: keyboard::Modifiers,
last_notified: Option<Viewport>,
+ last_scrolled: Option<Instant>,
}
impl Default for State {
@@ -1159,6 +1257,7 @@ impl Default for State {
x_scroller_grabbed_at: None,
keyboard_modifiers: keyboard::Modifiers::default(),
last_notified: None,
+ last_scrolled: None,
}
}
}
@@ -1171,6 +1270,15 @@ impl operation::Scrollable for State {
fn scroll_to(&mut self, offset: AbsoluteOffset) {
State::scroll_to(self, offset);
}
+
+ fn scroll_by(
+ &mut self,
+ offset: AbsoluteOffset,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) {
+ State::scroll_by(self, offset, bounds, content_bounds);
+ }
}
#[derive(Debug, Clone, Copy)]
@@ -1274,34 +1382,13 @@ impl State {
pub fn scroll(
&mut self,
delta: Vector<f32>,
- direction: Direction,
bounds: Rectangle,
content_bounds: Rectangle,
) {
- let horizontal_alignment = direction
- .horizontal()
- .map(|p| p.alignment)
- .unwrap_or_default();
-
- let vertical_alignment = direction
- .vertical()
- .map(|p| p.alignment)
- .unwrap_or_default();
-
- let align = |alignment: Anchor, delta: f32| match alignment {
- Anchor::Start => delta,
- Anchor::End => -delta,
- };
-
- let delta = Vector::new(
- align(horizontal_alignment, delta.x),
- align(vertical_alignment, delta.y),
- );
-
if bounds.height < content_bounds.height {
self.offset_y = Offset::Absolute(
(self.offset_y.absolute(bounds.height, content_bounds.height)
- - delta.y)
+ + delta.y)
.clamp(0.0, content_bounds.height - bounds.height),
);
}
@@ -1309,7 +1396,7 @@ impl State {
if bounds.width < content_bounds.width {
self.offset_x = Offset::Absolute(
(self.offset_x.absolute(bounds.width, content_bounds.width)
- - delta.x)
+ + delta.x)
.clamp(0.0, content_bounds.width - bounds.width),
);
}
@@ -1355,6 +1442,16 @@ impl State {
self.offset_y = Offset::Absolute(offset.y.max(0.0));
}
+ /// Scroll by the provided [`AbsoluteOffset`].
+ pub fn scroll_by(
+ &mut self,
+ offset: AbsoluteOffset,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) {
+ self.scroll(Vector::new(offset.x, offset.y), bounds, content_bounds);
+ }
+
/// Unsnaps the current scroll position, if snapped, given the bounds of the
/// [`Scrollable`] and its contents.
pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
diff --git a/widget/src/slider.rs b/widget/src/slider.rs
index 302cfae7..15514afe 100644
--- a/widget/src/slider.rs
+++ b/widget/src/slider.rs
@@ -9,8 +9,8 @@ use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- self, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle,
- Shell, Size, Theme, Widget,
+ self, Background, Clipboard, Color, Element, Layout, Length, Pixels, Point,
+ Rectangle, Shell, Size, Theme, Widget,
};
use std::ops::RangeInclusive;
@@ -426,10 +426,10 @@ where
width: offset + handle_width / 2.0,
height: style.rail.width,
},
- border: border::rounded(style.rail.border_radius),
+ border: style.rail.border,
..renderer::Quad::default()
},
- style.rail.colors.0,
+ style.rail.backgrounds.0,
);
renderer.fill_quad(
@@ -440,10 +440,10 @@ where
width: bounds.width - offset - handle_width / 2.0,
height: style.rail.width,
},
- border: border::rounded(style.rail.border_radius),
+ border: style.rail.border,
..renderer::Quad::default()
},
- style.rail.colors.1,
+ style.rail.backgrounds.1,
);
renderer.fill_quad(
@@ -461,7 +461,7 @@ where
},
..renderer::Quad::default()
},
- style.handle.color,
+ style.handle.background,
);
}
@@ -542,12 +542,12 @@ impl Style {
/// The appearance of a slider rail
#[derive(Debug, Clone, Copy)]
pub struct Rail {
- /// The colors of the rail of the slider.
- pub colors: (Color, Color),
+ /// The backgrounds of the rail of the slider.
+ pub backgrounds: (Background, Background),
/// The width of the stroke of a slider rail.
pub width: f32,
- /// The border radius of the corners of the rail.
- pub border_radius: border::Radius,
+ /// The border of the rail.
+ pub border: Border,
}
/// The appearance of the handle of a slider.
@@ -555,8 +555,8 @@ pub struct Rail {
pub struct Handle {
/// The shape of the handle.
pub shape: HandleShape,
- /// The [`Color`] of the handle.
- pub color: Color,
+ /// The [`Background`] of the handle.
+ pub background: Background,
/// The border width of the handle.
pub border_width: f32,
/// The border [`Color`] of the handle.
@@ -619,13 +619,17 @@ pub fn default(theme: &Theme, status: Status) -> Style {
Style {
rail: Rail {
- colors: (color, palette.secondary.base.color),
+ backgrounds: (color.into(), palette.secondary.base.color.into()),
width: 4.0,
- border_radius: 2.0.into(),
+ border: Border {
+ radius: 2.0.into(),
+ width: 0.0,
+ color: Color::TRANSPARENT,
+ },
},
handle: Handle {
shape: HandleShape::Circle { radius: 7.0 },
- color,
+ background: color.into(),
border_color: Color::TRANSPARENT,
border_width: 0.0,
},
diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs
index 1eb0d296..921c55a5 100644
--- a/widget/src/text/rich.rs
+++ b/widget/src/text/rich.rs
@@ -5,7 +5,7 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::text::{Paragraph, Span};
use crate::core::widget::text::{
- self, Catalog, LineHeight, Shaping, Style, StyleFn,
+ self, Catalog, LineHeight, Shaping, Style, StyleFn, Wrapping,
};
use crate::core::widget::tree::{self, Tree};
use crate::core::{
@@ -29,6 +29,7 @@ where
font: Option<Renderer::Font>,
align_x: alignment::Horizontal,
align_y: alignment::Vertical,
+ wrapping: Wrapping,
class: Theme::Class<'a>,
}
@@ -50,6 +51,7 @@ where
font: None,
align_x: alignment::Horizontal::Left,
align_y: alignment::Vertical::Top,
+ wrapping: Wrapping::default(),
class: Theme::default(),
}
}
@@ -118,6 +120,12 @@ where
self
}
+ /// Sets the [`Wrapping`] strategy of the [`Rich`] text.
+ pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
+ self.wrapping = wrapping;
+ self
+ }
+
/// Sets the default style of the [`Rich`] text.
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
@@ -218,6 +226,7 @@ where
self.font,
self.align_x,
self.align_y,
+ self.wrapping,
)
}
@@ -444,6 +453,7 @@ fn layout<Link, Renderer>(
font: Option<Renderer::Font>,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
+ wrapping: Wrapping,
) -> layout::Node
where
Link: Clone,
@@ -464,6 +474,7 @@ where
horizontal_alignment,
vertical_alignment,
shaping: Shaping::Advanced,
+ wrapping,
};
if state.spans != spans {
@@ -480,6 +491,7 @@ where
horizontal_alignment,
vertical_alignment,
shaping: Shaping::Advanced,
+ wrapping,
}) {
core::text::Difference::None => {}
core::text::Difference::Bounds => {
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
index 745e3ae8..1df97962 100644
--- a/widget/src/text_editor.rs
+++ b/widget/src/text_editor.rs
@@ -9,7 +9,7 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::text::editor::{Cursor, Editor as _};
use crate::core::text::highlighter::{self, Highlighter};
-use crate::core::text::{self, LineHeight, Text};
+use crate::core::text::{self, LineHeight, Text, Wrapping};
use crate::core::time::{Duration, Instant};
use crate::core::widget::operation;
use crate::core::widget::{self, Widget};
@@ -47,6 +47,7 @@ pub struct TextEditor<
width: Length,
height: Length,
padding: Padding,
+ wrapping: Wrapping,
class: Theme::Class<'a>,
key_binding: Option<Box<dyn Fn(KeyPress) -> Option<Binding<Message>> + 'a>>,
on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
@@ -74,6 +75,7 @@ where
width: Length::Fill,
height: Length::Shrink,
padding: Padding::new(5.0),
+ wrapping: Wrapping::default(),
class: Theme::default(),
key_binding: None,
on_edit: None,
@@ -107,6 +109,12 @@ where
self
}
+ /// Sets the width of the [`TextEditor`].
+ pub fn width(mut self, width: impl Into<Pixels>) -> Self {
+ self.width = Length::from(width.into());
+ self
+ }
+
/// Sets the message that should be produced when some action is performed in
/// the [`TextEditor`].
///
@@ -148,6 +156,12 @@ where
self
}
+ /// Sets the [`Wrapping`] strategy of the [`TextEditor`].
+ pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
+ self.wrapping = wrapping;
+ self
+ }
+
/// Highlights the [`TextEditor`] using the given syntax and theme.
#[cfg(feature = "highlighter")]
pub fn highlight(
@@ -186,6 +200,7 @@ where
width: self.width,
height: self.height,
padding: self.padding,
+ wrapping: self.wrapping,
class: self.class,
key_binding: self.key_binding,
on_edit: self.on_edit,
@@ -489,13 +504,14 @@ where
state.highlighter_settings = self.highlighter_settings.clone();
}
- let limits = limits.height(self.height);
+ let limits = limits.width(self.width).height(self.height);
internal.editor.update(
limits.shrink(self.padding).max(),
self.font.unwrap_or_else(|| renderer.default_font()),
self.text_size.unwrap_or_else(|| renderer.default_size()),
self.line_height,
+ self.wrapping,
state.highlighter.borrow_mut().deref_mut(),
);
@@ -784,6 +800,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
+ wrapping: self.wrapping,
},
text_bounds.position(),
style.placeholder,
@@ -964,7 +981,9 @@ impl<Message> Binding<Message> {
keyboard::Key::Named(key::Named::Backspace) => {
Some(Self::Backspace)
}
- keyboard::Key::Named(key::Named::Delete) => Some(Self::Delete),
+ keyboard::Key::Named(key::Named::Delete) if text.is_none() => {
+ Some(Self::Delete)
+ }
keyboard::Key::Named(key::Named::Escape) => Some(Self::Unfocus),
keyboard::Key::Character("c") if modifiers.command() => {
Some(Self::Copy)
@@ -1045,6 +1064,7 @@ impl<Message> Update<Message> {
let click = mouse::Click::new(
cursor_position,
+ mouse::Button::Left,
state.last_click,
);
diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs
index 2ac6f4ba..d5ede524 100644
--- a/widget/src/text_input.rs
+++ b/widget/src/text_input.rs
@@ -129,11 +129,23 @@ where
/// the [`TextInput`].
///
/// If this method is not called, the [`TextInput`] will be disabled.
- pub fn on_input<F>(mut self, callback: F) -> Self
- where
- F: 'a + Fn(String) -> Message,
- {
- self.on_input = Some(Box::new(callback));
+ pub fn on_input(
+ mut self,
+ on_input: impl Fn(String) -> Message + 'a,
+ ) -> Self {
+ self.on_input = Some(Box::new(on_input));
+ self
+ }
+
+ /// Sets the message that should be produced when some text is typed into
+ /// the [`TextInput`], if `Some`.
+ ///
+ /// If `None`, the [`TextInput`] will be disabled.
+ pub fn on_input_maybe(
+ mut self,
+ on_input: Option<impl Fn(String) -> Message + 'a>,
+ ) -> Self {
+ self.on_input = on_input.map(|f| Box::new(f) as _);
self
}
@@ -144,6 +156,13 @@ where
self
}
+ /// Sets the message that should be produced when the [`TextInput`] is
+ /// focused and the enter key is pressed, if `Some`.
+ pub fn on_submit_maybe(mut self, on_submit: Option<Message>) -> Self {
+ self.on_submit = on_submit;
+ self
+ }
+
/// Sets the message that should be produced when some text is pasted into
/// the [`TextInput`].
pub fn on_paste(
@@ -154,6 +173,16 @@ where
self
}
+ /// Sets the message that should be produced when some text is pasted into
+ /// the [`TextInput`], if `Some`.
+ pub fn on_paste_maybe(
+ mut self,
+ on_paste: Option<impl Fn(String) -> Message + 'a>,
+ ) -> Self {
+ self.on_paste = on_paste.map(|f| Box::new(f) as _);
+ self
+ }
+
/// Sets the [`Font`] of the [`TextInput`].
///
/// [`Font`]: text::Renderer::Font
@@ -251,6 +280,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
+ wrapping: text::Wrapping::default(),
};
state.placeholder.update(placeholder_text);
@@ -275,6 +305,7 @@ where
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
+ wrapping: text::Wrapping::default(),
};
state.icon.update(icon_text);
@@ -395,11 +426,11 @@ where
position,
);
- let is_cursor_visible = ((focus.now - focus.updated_at)
- .as_millis()
- / CURSOR_BLINK_INTERVAL_MILLIS)
- % 2
- == 0;
+ let is_cursor_visible = !is_disabled
+ && ((focus.now - focus.updated_at).as_millis()
+ / CURSOR_BLINK_INTERVAL_MILLIS)
+ % 2
+ == 0;
let cursor = if is_cursor_visible {
Some((
@@ -531,12 +562,9 @@ where
fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
- // Unfocus text input if it becomes disabled
+ // Stop pasting if input becomes disabled
if self.on_input.is_none() {
- state.last_click = None;
- state.is_focused = None;
state.is_pasting = None;
- state.is_dragging = false;
}
}
@@ -597,11 +625,7 @@ where
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state::<Renderer>(tree);
- let click_position = if self.on_input.is_some() {
- cursor.position_over(layout.bounds())
- } else {
- None
- };
+ let click_position = cursor.position_over(layout.bounds());
state.is_focused = if click_position.is_some() {
state.is_focused.or_else(|| {
@@ -632,8 +656,11 @@ where
cursor_position.x - text_bounds.x - alignment_offset
};
- let click =
- mouse::Click::new(cursor_position, state.last_click);
+ let click = mouse::Click::new(
+ cursor_position,
+ mouse::Button::Left,
+ state.last_click,
+ );
match click.kind() {
click::Kind::Single => {
@@ -747,10 +774,6 @@ where
let state = state::<Renderer>(tree);
if let Some(focus) = &mut state.is_focused {
- let Some(on_input) = &self.on_input else {
- return event::Status::Ignored;
- };
-
let modifiers = state.keyboard_modifiers;
focus.updated_at = Instant::now();
@@ -774,6 +797,10 @@ where
if state.keyboard_modifiers.command()
&& !self.is_secure =>
{
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
if let Some((start, end)) =
state.cursor.selection(&self.value)
{
@@ -798,6 +825,10 @@ where
if state.keyboard_modifiers.command()
&& !state.keyboard_modifiers.alt() =>
{
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
let content = match state.is_pasting.take() {
Some(content) => content,
None => {
@@ -841,6 +872,10 @@ where
}
if let Some(text) = text {
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
state.is_pasting = None;
if let Some(c) =
@@ -869,6 +904,10 @@ where
}
}
keyboard::Key::Named(key::Named::Backspace) => {
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
if modifiers.jump()
&& state.cursor.selection(&self.value).is_none()
{
@@ -893,6 +932,10 @@ where
update_cache(state, &self.value);
}
keyboard::Key::Named(key::Named::Delete) => {
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
if modifiers.jump()
&& state.cursor.selection(&self.value).is_none()
{
@@ -1111,7 +1154,7 @@ where
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) {
if self.on_input.is_none() {
- mouse::Interaction::NotAllowed
+ mouse::Interaction::Idle
} else {
mouse::Interaction::Text
}
@@ -1423,6 +1466,7 @@ fn replace_paragraph<Renderer>(
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
+ wrapping: text::Wrapping::default(),
});
}
diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs
index 821e2526..1c425dc1 100644
--- a/widget/src/toggler.rs
+++ b/widget/src/toggler.rs
@@ -26,7 +26,9 @@ use crate::core::{
///
/// let is_toggled = true;
///
-/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b));
+/// Toggler::new(is_toggled)
+/// .label("Toggle me!")
+/// .on_toggle(Message::TogglerToggled);
/// ```
#[allow(missing_debug_implementations)]
pub struct Toggler<
@@ -39,14 +41,15 @@ pub struct Toggler<
Renderer: text::Renderer,
{
is_toggled: bool,
- on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
- label: Option<String>,
+ on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
+ label: Option<text::Fragment<'a>>,
width: Length,
size: f32,
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_alignment: alignment::Horizontal,
text_shaping: text::Shaping,
+ text_wrapping: text::Wrapping,
spacing: f32,
font: Option<Renderer::Font>,
class: Theme::Class<'a>,
@@ -68,30 +71,54 @@ where
/// * a function that will be called when the [`Toggler`] is toggled. It
/// will receive the new state of the [`Toggler`] and must produce a
/// `Message`.
- pub fn new<F>(
- label: impl Into<Option<String>>,
- is_toggled: bool,
- f: F,
- ) -> Self
- where
- F: 'a + Fn(bool) -> Message,
- {
+ pub fn new(is_toggled: bool) -> Self {
Toggler {
is_toggled,
- on_toggle: Box::new(f),
- label: label.into(),
+ on_toggle: None,
+ label: None,
width: Length::Shrink,
size: Self::DEFAULT_SIZE,
text_size: None,
text_line_height: text::LineHeight::default(),
text_alignment: alignment::Horizontal::Left,
- text_shaping: text::Shaping::Basic,
+ text_shaping: text::Shaping::default(),
+ text_wrapping: text::Wrapping::default(),
spacing: Self::DEFAULT_SIZE / 2.0,
font: None,
class: Theme::default(),
}
}
+ /// Sets the label of the [`Toggler`].
+ pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
+ self.label = Some(label.into_fragment());
+ self
+ }
+
+ /// Sets the message that should be produced when a user toggles
+ /// the [`Toggler`].
+ ///
+ /// If this method is not called, the [`Toggler`] will be disabled.
+ pub fn on_toggle(
+ mut self,
+ on_toggle: impl Fn(bool) -> Message + 'a,
+ ) -> Self {
+ self.on_toggle = Some(Box::new(on_toggle));
+ self
+ }
+
+ /// Sets the message that should be produced when a user toggles
+ /// the [`Toggler`], if `Some`.
+ ///
+ /// If `None`, the [`Toggler`] will be disabled.
+ pub fn on_toggle_maybe(
+ mut self,
+ on_toggle: Option<impl Fn(bool) -> Message + 'a>,
+ ) -> Self {
+ self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
+ self
+ }
+
/// Sets the size of the [`Toggler`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.size = size.into().0;
@@ -131,6 +158,12 @@ where
self
}
+ /// Sets the [`text::Wrapping`] strategy of the [`Toggler`].
+ pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
+ self.text_wrapping = wrapping;
+ self
+ }
+
/// Sets the spacing between the [`Toggler`] and the text.
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
self.spacing = spacing.into().0;
@@ -216,6 +249,7 @@ where
self.text_alignment,
alignment::Vertical::Top,
self.text_shaping,
+ self.text_wrapping,
)
} else {
layout::Node::new(Size::ZERO)
@@ -235,13 +269,17 @@ where
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
+ let Some(on_toggle) = &self.on_toggle else {
+ return event::Status::Ignored;
+ };
+
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let mouse_over = cursor.is_over(layout.bounds());
if mouse_over {
- shell.publish((self.on_toggle)(!self.is_toggled));
+ shell.publish(on_toggle(!self.is_toggled));
event::Status::Captured
} else {
@@ -261,7 +299,11 @@ where
_renderer: &Renderer,
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) {
- mouse::Interaction::Pointer
+ if self.on_toggle.is_some() {
+ mouse::Interaction::Pointer
+ } else {
+ mouse::Interaction::NotAllowed
+ }
} else {
mouse::Interaction::default()
}
@@ -305,7 +347,9 @@ where
let bounds = toggler_layout.bounds();
let is_mouse_over = cursor.is_over(layout.bounds());
- let status = if is_mouse_over {
+ let status = if self.on_toggle.is_none() {
+ Status::Disabled
+ } else if is_mouse_over {
Status::Hovered {
is_toggled: self.is_toggled,
}
@@ -394,6 +438,8 @@ pub enum Status {
/// Indicates whether the [`Toggler`] is toggled.
is_toggled: bool,
},
+ /// The [`Toggler`] is disabled.
+ Disabled,
}
/// The appearance of a toggler.
@@ -454,6 +500,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
palette.background.strong.color
}
}
+ Status::Disabled => palette.background.weak.color,
};
let foreground = match status {
@@ -474,6 +521,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
palette.background.weak.color
}
}
+ Status::Disabled => palette.background.base.color,
};
Style {
diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs
index f02a490a..a75ba49c 100644
--- a/widget/src/vertical_slider.rs
+++ b/widget/src/vertical_slider.rs
@@ -5,7 +5,7 @@ pub use crate::slider::{
default, Catalog, Handle, HandleShape, Status, Style, StyleFn,
};
-use crate::core::border::{self, Border};
+use crate::core::border::Border;
use crate::core::event::{self, Event};
use crate::core::keyboard;
use crate::core::keyboard::key::{self, Key};
@@ -431,10 +431,10 @@ where
width: style.rail.width,
height: offset + handle_width / 2.0,
},
- border: border::rounded(style.rail.border_radius),
+ border: style.rail.border,
..renderer::Quad::default()
},
- style.rail.colors.1,
+ style.rail.backgrounds.1,
);
renderer.fill_quad(
@@ -445,10 +445,10 @@ where
width: style.rail.width,
height: bounds.height - offset - handle_width / 2.0,
},
- border: border::rounded(style.rail.border_radius),
+ border: style.rail.border,
..renderer::Quad::default()
},
- style.rail.colors.0,
+ style.rail.backgrounds.0,
);
renderer.fill_quad(
@@ -466,7 +466,7 @@ where
},
..renderer::Quad::default()
},
- style.handle.color,
+ style.handle.background,
);
}
diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs
index 7ae646fc..d54a1fe0 100644
--- a/winit/src/clipboard.rs
+++ b/winit/src/clipboard.rs
@@ -2,7 +2,7 @@
use crate::core::clipboard::Kind;
use std::sync::Arc;
-use winit::window::Window;
+use winit::window::{Window, WindowId};
/// A buffer for short-term storage and transfer within and between
/// applications.
@@ -83,6 +83,14 @@ impl Clipboard {
State::Unavailable => {}
}
}
+
+ /// Returns the identifier of the window used to create the [`Clipboard`], if any.
+ pub fn window_id(&self) -> Option<WindowId> {
+ match &self.state {
+ State::Connected { window, .. } => Some(window.id()),
+ State::Unavailable => None,
+ }
+ }
}
impl crate::core::Clipboard for Clipboard {
diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs
index e88ff84d..68f15b1a 100644
--- a/winit/src/conversion.rs
+++ b/winit/src/conversion.rs
@@ -79,6 +79,10 @@ pub fn window_attributes(
attributes = attributes
.with_skip_taskbar(settings.platform_specific.skip_taskbar);
+
+ attributes = attributes.with_undecorated_shadow(
+ settings.platform_specific.undecorated_shadow,
+ );
}
#[cfg(target_os = "macos")]
@@ -101,10 +105,14 @@ pub fn window_attributes(
{
use winit::platform::x11::WindowAttributesExtX11;
- attributes = attributes.with_name(
- &settings.platform_specific.application_id,
- &settings.platform_specific.application_id,
- );
+ attributes = attributes
+ .with_override_redirect(
+ settings.platform_specific.override_redirect,
+ )
+ .with_name(
+ &settings.platform_specific.application_id,
+ &settings.platform_specific.application_id,
+ );
}
#[cfg(feature = "wayland")]
{
@@ -184,7 +192,7 @@ pub fn window_event(
}
},
WindowEvent::KeyboardInput { event, .. } => Some(Event::Keyboard({
- let logical_key = {
+ let key = {
#[cfg(not(target_arch = "wasm32"))]
{
use winit::platform::modifier_supplement::KeyEventExtModifierSupplement;
@@ -194,7 +202,7 @@ pub fn window_event(
#[cfg(target_arch = "wasm32")]
{
// TODO: Fix inconsistent API on Wasm
- event.logical_key
+ event.logical_key.clone()
}
};
@@ -215,9 +223,16 @@ pub fn window_event(
}.filter(|text| !text.as_str().chars().any(is_private_use));
let winit::event::KeyEvent {
- state, location, ..
+ state,
+ location,
+ logical_key,
+ physical_key,
+ ..
} = event;
- let key = key(logical_key);
+
+ let key = self::key(key);
+ let modified_key = self::key(logical_key);
+ let physical_key = self::physical_key(physical_key);
let modifiers = self::modifiers(modifiers);
let location = match location {
@@ -237,6 +252,8 @@ pub fn window_event(
winit::event::ElementState::Pressed => {
keyboard::Event::KeyPressed {
key,
+ modified_key,
+ physical_key,
modifiers,
location,
text,
@@ -423,8 +440,19 @@ pub fn mouse_interaction(
winit::window::CursorIcon::EwResize
}
Interaction::ResizingVertically => winit::window::CursorIcon::NsResize,
+ Interaction::ResizingDiagonallyUp => {
+ winit::window::CursorIcon::NeswResize
+ }
+ Interaction::ResizingDiagonallyDown => {
+ winit::window::CursorIcon::NwseResize
+ }
Interaction::NotAllowed => winit::window::CursorIcon::NotAllowed,
Interaction::ZoomIn => winit::window::CursorIcon::ZoomIn,
+ Interaction::ZoomOut => winit::window::CursorIcon::ZoomOut,
+ Interaction::Cell => winit::window::CursorIcon::Cell,
+ Interaction::Move => winit::window::CursorIcon::Move,
+ Interaction::Copy => winit::window::CursorIcon::Copy,
+ Interaction::Help => winit::window::CursorIcon::Help,
}
}
@@ -502,7 +530,7 @@ pub fn touch_event(
}
}
-/// Converts a `VirtualKeyCode` from [`winit`] to an [`iced`] key code.
+/// Converts a `Key` from [`winit`] to an [`iced`] key.
///
/// [`winit`]: https://github.com/rust-windowing/winit
/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12
@@ -831,6 +859,257 @@ pub fn key(key: winit::keyboard::Key) -> keyboard::Key {
}
}
+/// Converts a `PhysicalKey` from [`winit`] to an [`iced`] physical key.
+///
+/// [`winit`]: https://github.com/rust-windowing/winit
+/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12
+pub fn physical_key(
+ physical_key: winit::keyboard::PhysicalKey,
+) -> keyboard::key::Physical {
+ match physical_key {
+ winit::keyboard::PhysicalKey::Code(code) => key_code(code)
+ .map(keyboard::key::Physical::Code)
+ .unwrap_or(keyboard::key::Physical::Unidentified(
+ keyboard::key::NativeCode::Unidentified,
+ )),
+ winit::keyboard::PhysicalKey::Unidentified(code) => {
+ keyboard::key::Physical::Unidentified(native_key_code(code))
+ }
+ }
+}
+
+/// Converts a `KeyCode` from [`winit`] to an [`iced`] key code.
+///
+/// [`winit`]: https://github.com/rust-windowing/winit
+/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12
+pub fn key_code(
+ key_code: winit::keyboard::KeyCode,
+) -> Option<keyboard::key::Code> {
+ use winit::keyboard::KeyCode;
+
+ Some(match key_code {
+ KeyCode::Backquote => keyboard::key::Code::Backquote,
+ KeyCode::Backslash => keyboard::key::Code::Backslash,
+ KeyCode::BracketLeft => keyboard::key::Code::BracketLeft,
+ KeyCode::BracketRight => keyboard::key::Code::BracketRight,
+ KeyCode::Comma => keyboard::key::Code::Comma,
+ KeyCode::Digit0 => keyboard::key::Code::Digit0,
+ KeyCode::Digit1 => keyboard::key::Code::Digit1,
+ KeyCode::Digit2 => keyboard::key::Code::Digit2,
+ KeyCode::Digit3 => keyboard::key::Code::Digit3,
+ KeyCode::Digit4 => keyboard::key::Code::Digit4,
+ KeyCode::Digit5 => keyboard::key::Code::Digit5,
+ KeyCode::Digit6 => keyboard::key::Code::Digit6,
+ KeyCode::Digit7 => keyboard::key::Code::Digit7,
+ KeyCode::Digit8 => keyboard::key::Code::Digit8,
+ KeyCode::Digit9 => keyboard::key::Code::Digit9,
+ KeyCode::Equal => keyboard::key::Code::Equal,
+ KeyCode::IntlBackslash => keyboard::key::Code::IntlBackslash,
+ KeyCode::IntlRo => keyboard::key::Code::IntlRo,
+ KeyCode::IntlYen => keyboard::key::Code::IntlYen,
+ KeyCode::KeyA => keyboard::key::Code::KeyA,
+ KeyCode::KeyB => keyboard::key::Code::KeyB,
+ KeyCode::KeyC => keyboard::key::Code::KeyC,
+ KeyCode::KeyD => keyboard::key::Code::KeyD,
+ KeyCode::KeyE => keyboard::key::Code::KeyE,
+ KeyCode::KeyF => keyboard::key::Code::KeyF,
+ KeyCode::KeyG => keyboard::key::Code::KeyG,
+ KeyCode::KeyH => keyboard::key::Code::KeyH,
+ KeyCode::KeyI => keyboard::key::Code::KeyI,
+ KeyCode::KeyJ => keyboard::key::Code::KeyJ,
+ KeyCode::KeyK => keyboard::key::Code::KeyK,
+ KeyCode::KeyL => keyboard::key::Code::KeyL,
+ KeyCode::KeyM => keyboard::key::Code::KeyM,
+ KeyCode::KeyN => keyboard::key::Code::KeyN,
+ KeyCode::KeyO => keyboard::key::Code::KeyO,
+ KeyCode::KeyP => keyboard::key::Code::KeyP,
+ KeyCode::KeyQ => keyboard::key::Code::KeyQ,
+ KeyCode::KeyR => keyboard::key::Code::KeyR,
+ KeyCode::KeyS => keyboard::key::Code::KeyS,
+ KeyCode::KeyT => keyboard::key::Code::KeyT,
+ KeyCode::KeyU => keyboard::key::Code::KeyU,
+ KeyCode::KeyV => keyboard::key::Code::KeyV,
+ KeyCode::KeyW => keyboard::key::Code::KeyW,
+ KeyCode::KeyX => keyboard::key::Code::KeyX,
+ KeyCode::KeyY => keyboard::key::Code::KeyY,
+ KeyCode::KeyZ => keyboard::key::Code::KeyZ,
+ KeyCode::Minus => keyboard::key::Code::Minus,
+ KeyCode::Period => keyboard::key::Code::Period,
+ KeyCode::Quote => keyboard::key::Code::Quote,
+ KeyCode::Semicolon => keyboard::key::Code::Semicolon,
+ KeyCode::Slash => keyboard::key::Code::Slash,
+ KeyCode::AltLeft => keyboard::key::Code::AltLeft,
+ KeyCode::AltRight => keyboard::key::Code::AltRight,
+ KeyCode::Backspace => keyboard::key::Code::Backspace,
+ KeyCode::CapsLock => keyboard::key::Code::CapsLock,
+ KeyCode::ContextMenu => keyboard::key::Code::ContextMenu,
+ KeyCode::ControlLeft => keyboard::key::Code::ControlLeft,
+ KeyCode::ControlRight => keyboard::key::Code::ControlRight,
+ KeyCode::Enter => keyboard::key::Code::Enter,
+ KeyCode::SuperLeft => keyboard::key::Code::SuperLeft,
+ KeyCode::SuperRight => keyboard::key::Code::SuperRight,
+ KeyCode::ShiftLeft => keyboard::key::Code::ShiftLeft,
+ KeyCode::ShiftRight => keyboard::key::Code::ShiftRight,
+ KeyCode::Space => keyboard::key::Code::Space,
+ KeyCode::Tab => keyboard::key::Code::Tab,
+ KeyCode::Convert => keyboard::key::Code::Convert,
+ KeyCode::KanaMode => keyboard::key::Code::KanaMode,
+ KeyCode::Lang1 => keyboard::key::Code::Lang1,
+ KeyCode::Lang2 => keyboard::key::Code::Lang2,
+ KeyCode::Lang3 => keyboard::key::Code::Lang3,
+ KeyCode::Lang4 => keyboard::key::Code::Lang4,
+ KeyCode::Lang5 => keyboard::key::Code::Lang5,
+ KeyCode::NonConvert => keyboard::key::Code::NonConvert,
+ KeyCode::Delete => keyboard::key::Code::Delete,
+ KeyCode::End => keyboard::key::Code::End,
+ KeyCode::Help => keyboard::key::Code::Help,
+ KeyCode::Home => keyboard::key::Code::Home,
+ KeyCode::Insert => keyboard::key::Code::Insert,
+ KeyCode::PageDown => keyboard::key::Code::PageDown,
+ KeyCode::PageUp => keyboard::key::Code::PageUp,
+ KeyCode::ArrowDown => keyboard::key::Code::ArrowDown,
+ KeyCode::ArrowLeft => keyboard::key::Code::ArrowLeft,
+ KeyCode::ArrowRight => keyboard::key::Code::ArrowRight,
+ KeyCode::ArrowUp => keyboard::key::Code::ArrowUp,
+ KeyCode::NumLock => keyboard::key::Code::NumLock,
+ KeyCode::Numpad0 => keyboard::key::Code::Numpad0,
+ KeyCode::Numpad1 => keyboard::key::Code::Numpad1,
+ KeyCode::Numpad2 => keyboard::key::Code::Numpad2,
+ KeyCode::Numpad3 => keyboard::key::Code::Numpad3,
+ KeyCode::Numpad4 => keyboard::key::Code::Numpad4,
+ KeyCode::Numpad5 => keyboard::key::Code::Numpad5,
+ KeyCode::Numpad6 => keyboard::key::Code::Numpad6,
+ KeyCode::Numpad7 => keyboard::key::Code::Numpad7,
+ KeyCode::Numpad8 => keyboard::key::Code::Numpad8,
+ KeyCode::Numpad9 => keyboard::key::Code::Numpad9,
+ KeyCode::NumpadAdd => keyboard::key::Code::NumpadAdd,
+ KeyCode::NumpadBackspace => keyboard::key::Code::NumpadBackspace,
+ KeyCode::NumpadClear => keyboard::key::Code::NumpadClear,
+ KeyCode::NumpadClearEntry => keyboard::key::Code::NumpadClearEntry,
+ KeyCode::NumpadComma => keyboard::key::Code::NumpadComma,
+ KeyCode::NumpadDecimal => keyboard::key::Code::NumpadDecimal,
+ KeyCode::NumpadDivide => keyboard::key::Code::NumpadDivide,
+ KeyCode::NumpadEnter => keyboard::key::Code::NumpadEnter,
+ KeyCode::NumpadEqual => keyboard::key::Code::NumpadEqual,
+ KeyCode::NumpadHash => keyboard::key::Code::NumpadHash,
+ KeyCode::NumpadMemoryAdd => keyboard::key::Code::NumpadMemoryAdd,
+ KeyCode::NumpadMemoryClear => keyboard::key::Code::NumpadMemoryClear,
+ KeyCode::NumpadMemoryRecall => keyboard::key::Code::NumpadMemoryRecall,
+ KeyCode::NumpadMemoryStore => keyboard::key::Code::NumpadMemoryStore,
+ KeyCode::NumpadMemorySubtract => {
+ keyboard::key::Code::NumpadMemorySubtract
+ }
+ KeyCode::NumpadMultiply => keyboard::key::Code::NumpadMultiply,
+ KeyCode::NumpadParenLeft => keyboard::key::Code::NumpadParenLeft,
+ KeyCode::NumpadParenRight => keyboard::key::Code::NumpadParenRight,
+ KeyCode::NumpadStar => keyboard::key::Code::NumpadStar,
+ KeyCode::NumpadSubtract => keyboard::key::Code::NumpadSubtract,
+ KeyCode::Escape => keyboard::key::Code::Escape,
+ KeyCode::Fn => keyboard::key::Code::Fn,
+ KeyCode::FnLock => keyboard::key::Code::FnLock,
+ KeyCode::PrintScreen => keyboard::key::Code::PrintScreen,
+ KeyCode::ScrollLock => keyboard::key::Code::ScrollLock,
+ KeyCode::Pause => keyboard::key::Code::Pause,
+ KeyCode::BrowserBack => keyboard::key::Code::BrowserBack,
+ KeyCode::BrowserFavorites => keyboard::key::Code::BrowserFavorites,
+ KeyCode::BrowserForward => keyboard::key::Code::BrowserForward,
+ KeyCode::BrowserHome => keyboard::key::Code::BrowserHome,
+ KeyCode::BrowserRefresh => keyboard::key::Code::BrowserRefresh,
+ KeyCode::BrowserSearch => keyboard::key::Code::BrowserSearch,
+ KeyCode::BrowserStop => keyboard::key::Code::BrowserStop,
+ KeyCode::Eject => keyboard::key::Code::Eject,
+ KeyCode::LaunchApp1 => keyboard::key::Code::LaunchApp1,
+ KeyCode::LaunchApp2 => keyboard::key::Code::LaunchApp2,
+ KeyCode::LaunchMail => keyboard::key::Code::LaunchMail,
+ KeyCode::MediaPlayPause => keyboard::key::Code::MediaPlayPause,
+ KeyCode::MediaSelect => keyboard::key::Code::MediaSelect,
+ KeyCode::MediaStop => keyboard::key::Code::MediaStop,
+ KeyCode::MediaTrackNext => keyboard::key::Code::MediaTrackNext,
+ KeyCode::MediaTrackPrevious => keyboard::key::Code::MediaTrackPrevious,
+ KeyCode::Power => keyboard::key::Code::Power,
+ KeyCode::Sleep => keyboard::key::Code::Sleep,
+ KeyCode::AudioVolumeDown => keyboard::key::Code::AudioVolumeDown,
+ KeyCode::AudioVolumeMute => keyboard::key::Code::AudioVolumeMute,
+ KeyCode::AudioVolumeUp => keyboard::key::Code::AudioVolumeUp,
+ KeyCode::WakeUp => keyboard::key::Code::WakeUp,
+ KeyCode::Meta => keyboard::key::Code::Meta,
+ KeyCode::Hyper => keyboard::key::Code::Hyper,
+ KeyCode::Turbo => keyboard::key::Code::Turbo,
+ KeyCode::Abort => keyboard::key::Code::Abort,
+ KeyCode::Resume => keyboard::key::Code::Resume,
+ KeyCode::Suspend => keyboard::key::Code::Suspend,
+ KeyCode::Again => keyboard::key::Code::Again,
+ KeyCode::Copy => keyboard::key::Code::Copy,
+ KeyCode::Cut => keyboard::key::Code::Cut,
+ KeyCode::Find => keyboard::key::Code::Find,
+ KeyCode::Open => keyboard::key::Code::Open,
+ KeyCode::Paste => keyboard::key::Code::Paste,
+ KeyCode::Props => keyboard::key::Code::Props,
+ KeyCode::Select => keyboard::key::Code::Select,
+ KeyCode::Undo => keyboard::key::Code::Undo,
+ KeyCode::Hiragana => keyboard::key::Code::Hiragana,
+ KeyCode::Katakana => keyboard::key::Code::Katakana,
+ KeyCode::F1 => keyboard::key::Code::F1,
+ KeyCode::F2 => keyboard::key::Code::F2,
+ KeyCode::F3 => keyboard::key::Code::F3,
+ KeyCode::F4 => keyboard::key::Code::F4,
+ KeyCode::F5 => keyboard::key::Code::F5,
+ KeyCode::F6 => keyboard::key::Code::F6,
+ KeyCode::F7 => keyboard::key::Code::F7,
+ KeyCode::F8 => keyboard::key::Code::F8,
+ KeyCode::F9 => keyboard::key::Code::F9,
+ KeyCode::F10 => keyboard::key::Code::F10,
+ KeyCode::F11 => keyboard::key::Code::F11,
+ KeyCode::F12 => keyboard::key::Code::F12,
+ KeyCode::F13 => keyboard::key::Code::F13,
+ KeyCode::F14 => keyboard::key::Code::F14,
+ KeyCode::F15 => keyboard::key::Code::F15,
+ KeyCode::F16 => keyboard::key::Code::F16,
+ KeyCode::F17 => keyboard::key::Code::F17,
+ KeyCode::F18 => keyboard::key::Code::F18,
+ KeyCode::F19 => keyboard::key::Code::F19,
+ KeyCode::F20 => keyboard::key::Code::F20,
+ KeyCode::F21 => keyboard::key::Code::F21,
+ KeyCode::F22 => keyboard::key::Code::F22,
+ KeyCode::F23 => keyboard::key::Code::F23,
+ KeyCode::F24 => keyboard::key::Code::F24,
+ KeyCode::F25 => keyboard::key::Code::F25,
+ KeyCode::F26 => keyboard::key::Code::F26,
+ KeyCode::F27 => keyboard::key::Code::F27,
+ KeyCode::F28 => keyboard::key::Code::F28,
+ KeyCode::F29 => keyboard::key::Code::F29,
+ KeyCode::F30 => keyboard::key::Code::F30,
+ KeyCode::F31 => keyboard::key::Code::F31,
+ KeyCode::F32 => keyboard::key::Code::F32,
+ KeyCode::F33 => keyboard::key::Code::F33,
+ KeyCode::F34 => keyboard::key::Code::F34,
+ KeyCode::F35 => keyboard::key::Code::F35,
+ _ => None?,
+ })
+}
+
+/// Converts a `NativeKeyCode` from [`winit`] to an [`iced`] native key code.
+///
+/// [`winit`]: https://github.com/rust-windowing/winit
+/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12
+pub fn native_key_code(
+ native_key_code: winit::keyboard::NativeKeyCode,
+) -> keyboard::key::NativeCode {
+ use winit::keyboard::NativeKeyCode;
+
+ match native_key_code {
+ NativeKeyCode::Unidentified => keyboard::key::NativeCode::Unidentified,
+ NativeKeyCode::Android(code) => {
+ keyboard::key::NativeCode::Android(code)
+ }
+ NativeKeyCode::MacOS(code) => keyboard::key::NativeCode::MacOS(code),
+ NativeKeyCode::Windows(code) => {
+ keyboard::key::NativeCode::Windows(code)
+ }
+ NativeKeyCode::Xkb(code) => keyboard::key::NativeCode::Xkb(code),
+ }
+}
+
/// Converts some [`UserAttention`] into it's `winit` counterpart.
///
/// [`UserAttention`]: window::UserAttention
diff --git a/winit/src/program.rs b/winit/src/program.rs
index 54221c68..eef7e6c6 100644
--- a/winit/src/program.rs
+++ b/winit/src/program.rs
@@ -219,7 +219,7 @@ where
}
runtime.track(subscription::into_recipes(
- program.subscription().map(Action::Output),
+ runtime.enter(|| program.subscription().map(Action::Output)),
));
let (boot_sender, boot_receiver) = oneshot::channel();
@@ -307,8 +307,6 @@ where
}
};
- let clipboard = Clipboard::connect(window.clone());
-
let finish_boot = async move {
let mut compositor =
C::new(graphics_settings, window.clone()).await?;
@@ -318,10 +316,7 @@ where
}
sender
- .send(Boot {
- compositor,
- clipboard,
- })
+ .send(Boot { compositor })
.ok()
.expect("Send boot event");
@@ -617,7 +612,6 @@ where
struct Boot<C> {
compositor: C,
- clipboard: Clipboard,
}
#[derive(Debug)]
@@ -662,10 +656,7 @@ async fn run_instance<P, C>(
use winit::event;
use winit::event_loop::ControlFlow;
- let Boot {
- mut compositor,
- mut clipboard,
- } = boot.await.expect("Receive boot");
+ let Boot { mut compositor } = boot.await.expect("Receive boot");
let mut window_manager = WindowManager::new();
let mut is_window_opening = !is_daemon;
@@ -676,6 +667,7 @@ async fn run_instance<P, C>(
let mut ui_caches = FxHashMap::default();
let mut user_interfaces = ManuallyDrop::new(FxHashMap::default());
+ let mut clipboard = Clipboard::unconnected();
debug.startup_finished();
@@ -734,6 +726,10 @@ async fn run_instance<P, C>(
}),
));
+ if clipboard.window_id().is_none() {
+ clipboard = Clipboard::connect(window.raw.clone());
+ }
+
let _ = on_open.send(id);
is_window_opening = false;
}
@@ -979,14 +975,22 @@ async fn run_instance<P, C>(
winit::event::WindowEvent::CloseRequested
) && window.exit_on_close_request
{
- let _ = window_manager.remove(id);
- let _ = user_interfaces.remove(&id);
- let _ = ui_caches.remove(&id);
-
- events.push((
- id,
- core::Event::Window(window::Event::Closed),
- ));
+ run_action(
+ Action::Window(runtime::window::Action::Close(
+ id,
+ )),
+ &program,
+ &mut compositor,
+ &mut events,
+ &mut messages,
+ &mut clipboard,
+ &mut control_sender,
+ &mut debug,
+ &mut user_interfaces,
+ &mut window_manager,
+ &mut ui_caches,
+ &mut is_window_opening,
+ );
} else {
window.state.update(
&window.raw,
@@ -1165,7 +1169,7 @@ fn update<P: Program, E: Executor>(
}
}
- let subscription = program.subscription();
+ let subscription = runtime.enter(|| program.subscription());
runtime.track(subscription::into_recipes(subscription.map(Action::Output)));
}
@@ -1223,10 +1227,18 @@ fn run_action<P, C>(
*is_window_opening = true;
}
window::Action::Close(id) => {
- let window = window_manager.remove(id);
let _ = ui_caches.remove(&id);
+ let _ = interfaces.remove(&id);
+
+ if let Some(window) = window_manager.remove(id) {
+ if clipboard.window_id() == Some(window.raw.id()) {
+ *clipboard = window_manager
+ .first()
+ .map(|window| window.raw.clone())
+ .map(Clipboard::connect)
+ .unwrap_or_else(Clipboard::unconnected);
+ }
- if window.is_some() {
events.push((
id,
core::Event::Window(core::window::Event::Closed),
@@ -1423,6 +1435,16 @@ fn run_action<P, C>(
));
}
}
+ window::Action::EnableMousePassthrough(id) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ let _ = window.raw.set_cursor_hittest(false);
+ }
+ }
+ window::Action::DisableMousePassthrough(id) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ let _ = window.raw.set_cursor_hittest(true);
+ }
+ }
},
Action::System(action) => match action {
system::Action::QueryInformation(_channel) => {
diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs
index 8cd9fab7..3d22e155 100644
--- a/winit/src/program/window_manager.rs
+++ b/winit/src/program/window_manager.rs
@@ -74,6 +74,10 @@ where
self.entries.is_empty()
}
+ pub fn first(&self) -> Option<&Window<P, C>> {
+ self.entries.first_key_value().map(|(_id, window)| window)
+ }
+
pub fn iter_mut(
&mut self,
) -> impl Iterator<Item = (Id, &mut Window<P, C>)> {