summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml12
-rw-r--r--Cargo.toml6
-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/padding.rs2
-rw-r--r--core/src/renderer/null.rs1
-rw-r--r--core/src/text.rs21
-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/markdown/src/main.rs18
-rw-r--r--examples/multitouch/src/main.rs2
-rw-r--r--examples/pane_grid/src/main.rs22
-rw-r--r--examples/styling/src/main.rs10
-rw-r--r--examples/todos/src/main.rs3
-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.rs38
-rw-r--r--tiny_skia/src/geometry.rs9
-rw-r--r--wgpu/src/geometry.rs38
-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.rs13
-rw-r--r--widget/src/helpers.rs9
-rw-r--r--widget/src/lazy.rs2
-rw-r--r--widget/src/lazy/component.rs7
-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/markdown.rs275
-rw-r--r--widget/src/mouse_area.rs49
-rw-r--r--widget/src/overlay/menu.rs1
-rw-r--r--widget/src/pane_grid.rs2
-rw-r--r--widget/src/pane_grid/controls.rs59
-rw-r--r--widget/src/pane_grid/title_bar.rs317
-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.rs49
-rw-r--r--widget/src/text_editor.rs26
-rw-r--r--widget/src/text_input.rs175
-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.rs286
-rw-r--r--winit/src/program.rs77
-rw-r--r--winit/src/program/window_manager.rs8
73 files changed, 2630 insertions, 712 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/Cargo.toml b/Cargo.toml
index 65c5007e..52464e38 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,7 +27,9 @@ wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"]
# Enable the `tiny-skia` software renderer backend
tiny-skia = ["iced_renderer/tiny-skia"]
# Enables the `Image` widget
-image = ["iced_widget/image", "dep:image"]
+image = ["image-without-codecs", "image/default"]
+# Enables the `Image` widget, without any built-in codecs of the `image` crate
+image-without-codecs = ["iced_widget/image", "dep:image"]
# Enables the `Svg` widget
svg = ["iced_widget/svg"]
# Enables the `Canvas` widget
@@ -147,7 +149,7 @@ glam = "0.25"
glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "feef9f5630c2adb3528937e55f7bfad2da561a65" }
guillotiere = "0.6"
half = "2.2"
-image = "0.24"
+image = { version = "0.24", default-features = false }
kamadak-exif = "0.5"
kurbo = "0.10"
log = "0.4"
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/padding.rs b/core/src/padding.rs
index fdaa0236..e26cdd9b 100644
--- a/core/src/padding.rs
+++ b/core/src/padding.rs
@@ -32,7 +32,7 @@ use crate::{Pixels, Size};
/// let widget = Widget::new().padding(20); // 20px on all sides
/// let widget = Widget::new().padding([10, 20]); // top/bottom, left/right
/// ```
-#[derive(Debug, Copy, Clone, Default)]
+#[derive(Debug, Copy, Clone, PartialEq, Default)]
pub struct Padding {
/// Top padding
pub top: f32,
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 436fee9a..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 {
@@ -252,7 +271,7 @@ pub struct Span<'a, Link = (), Font = crate::Font> {
}
/// A text highlight.
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Highlight {
/// The [`Background`] of the highlight.
pub background: Background,
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/markdown/src/main.rs b/examples/markdown/src/main.rs
index eb51f985..5605478f 100644
--- a/examples/markdown/src/main.rs
+++ b/examples/markdown/src/main.rs
@@ -29,8 +29,7 @@ impl Markdown {
(
Self {
content: text_editor::Content::with_text(INITIAL_CONTENT),
- items: markdown::parse(INITIAL_CONTENT, theme.palette())
- .collect(),
+ items: markdown::parse(INITIAL_CONTENT).collect(),
theme,
},
widget::focus_next(),
@@ -45,11 +44,8 @@ impl Markdown {
self.content.perform(action);
if is_edit {
- self.items = markdown::parse(
- &self.content.text(),
- self.theme.palette(),
- )
- .collect();
+ self.items =
+ markdown::parse(&self.content.text()).collect();
}
}
Message::LinkClicked(link) => {
@@ -67,8 +63,12 @@ impl Markdown {
.font(Font::MONOSPACE)
.highlight("markdown", highlighter::Theme::Base16Ocean);
- let preview = markdown(&self.items, markdown::Settings::default())
- .map(Message::LinkClicked);
+ let preview = markdown(
+ &self.items,
+ markdown::Settings::default(),
+ markdown::Style::from_palette(self.theme.palette()),
+ )
+ .map(Message::LinkClicked);
row![editor, scrollable(preview).spacing(10).height(Fill)]
.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/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs
index f18fc5f3..67f4d27f 100644
--- a/examples/pane_grid/src/main.rs
+++ b/examples/pane_grid/src/main.rs
@@ -154,11 +154,23 @@ impl Example {
.spacing(5);
let title_bar = pane_grid::TitleBar::new(title)
- .controls(view_controls(
- id,
- total_panes,
- pane.is_pinned,
- is_maximized,
+ .controls(pane_grid::Controls::dynamic(
+ view_controls(
+ id,
+ total_panes,
+ pane.is_pinned,
+ is_maximized,
+ ),
+ button(text("X").size(14))
+ .style(button::danger)
+ .padding(3)
+ .on_press_maybe(
+ if total_panes > 1 && !pane.is_pinned {
+ Some(Message::Close(id))
+ } else {
+ None
+ },
+ ),
))
.padding(10)
.style(if is_focused {
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/todos/src/main.rs b/examples/todos/src/main.rs
index 86845f87..a5f7b36a 100644
--- a/examples/todos/src/main.rs
+++ b/examples/todos/src/main.rs
@@ -202,7 +202,8 @@ impl Todos {
.on_input(Message::InputChanged)
.on_submit(Message::CreateTask)
.padding(15)
- .size(30);
+ .size(30)
+ .align_x(Center);
let controls = view_controls(tasks, *filter);
let filtered_tasks =
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 cd27cdfe..cdf3d80a 100644
--- a/runtime/src/window.rs
+++ b/runtime/src/window.rs
@@ -63,6 +63,9 @@ pub enum Action {
/// Get the current logical coordinates of the window.
GetPosition(Id, oneshot::Sender<Option<Point>>),
+ /// Get the current scale factor (DPI) of the window.
+ GetScaleFactor(Id, oneshot::Sender<f32>),
+
/// Move the window to the given logical coordinates.
///
/// Unsupported on Wayland.
@@ -144,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.
@@ -292,6 +307,13 @@ pub fn get_position(id: Id) -> Task<Option<Point>> {
})
}
+/// Gets the scale factor of the window with the given [`Id`].
+pub fn get_scale_factor(id: Id) -> Task<f32> {
+ task::oneshot(move |channel| {
+ crate::Action::Window(Action::GetScaleFactor(id, channel))
+ })
+}
+
/// Moves the window to the given logical coordinates.
pub fn move_to<T>(id: Id, position: Point) -> Task<T> {
task::effect(crate::Action::Window(Action::Move(id, position)))
@@ -396,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/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/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 54043ad0..3b794099 100644
--- a/widget/src/container.rs
+++ b/widget/src/container.rs
@@ -184,7 +184,6 @@ where
}
/// Sets the style class of the [`Container`].
- #[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
@@ -460,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() {
@@ -613,6 +613,12 @@ pub trait Catalog {
/// A styling function for a [`Container`].
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
+impl<'a, Theme> From<Style> for StyleFn<'a, Theme> {
+ fn from(style: Style) -> Self {
+ Box::new(move |_theme| style)
+ }
+}
+
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
@@ -630,6 +636,11 @@ pub fn transparent<Theme>(_theme: &Theme) -> Style {
Style::default()
}
+/// A [`Container`] with the given [`Background`].
+pub fn background(background: impl Into<Background>) -> Style {
+ Style::default().background(background)
+}
+
/// A rounded [`Container`] with a background.
pub fn rounded_box(theme: &Theme) -> Style {
let palette = theme.extended_palette();
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index c3ffea45..51978823 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -25,7 +25,7 @@ use crate::tooltip::{self, Tooltip};
use crate::vertical_slider::{self, VerticalSlider};
use crate::{Column, MouseArea, Row, Space, Stack, Themer};
-use std::borrow::{Borrow, Cow};
+use std::borrow::Borrow;
use std::ops::RangeInclusive;
/// Creates a [`Column`] with the given children.
@@ -707,12 +707,13 @@ where
///
/// [`Rich`]: text::Rich
pub fn rich_text<'a, Link, Theme, Renderer>(
- spans: impl Into<Cow<'a, [text::Span<'a, Link, Renderer::Font>]>>,
+ spans: impl AsRef<[text::Span<'a, Link, Renderer::Font>]> + 'a,
) -> text::Rich<'a, Link, Theme, Renderer>
where
Link: Clone + 'static,
Theme: text::Catalog + 'a,
Renderer: core::text::Renderer,
+ Renderer::Font: 'a,
{
text::Rich::with_spans(spans)
}
@@ -766,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..659bc476 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,12 @@ use std::rc::Rc;
///
/// Additionally, a [`Component`] is capable of producing a `Message` to notify
/// the parent application of any relevant interactions.
+#[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/markdown.rs b/widget/src/markdown.rs
index 23e36435..fa4ee6bf 100644
--- a/widget/src/markdown.rs
+++ b/widget/src/markdown.rs
@@ -7,10 +7,16 @@
use crate::core::border;
use crate::core::font::{self, Font};
use crate::core::padding;
-use crate::core::theme::{self, Theme};
-use crate::core::{self, color, Color, Element, Length, Pixels};
+use crate::core::theme;
+use crate::core::{
+ self, color, Color, Element, Length, Padding, Pixels, Theme,
+};
use crate::{column, container, rich_text, row, scrollable, span, text};
+use std::cell::{Cell, RefCell};
+use std::rc::Rc;
+
+pub use core::text::Highlight;
pub use pulldown_cmark::HeadingLevel;
pub use url::Url;
@@ -18,13 +24,13 @@ pub use url::Url;
#[derive(Debug, Clone)]
pub enum Item {
/// A heading.
- Heading(pulldown_cmark::HeadingLevel, Vec<text::Span<'static, Url>>),
+ Heading(pulldown_cmark::HeadingLevel, Text),
/// A paragraph.
- Paragraph(Vec<text::Span<'static, Url>>),
+ Paragraph(Text),
/// A code block.
///
/// You can enable the `highlighter` feature for syntax highligting.
- CodeBlock(Vec<text::Span<'static, Url>>),
+ CodeBlock(Text),
/// A list.
List {
/// The first number of the list, if it is ordered.
@@ -34,11 +40,112 @@ pub enum Item {
},
}
+/// A bunch of parsed Markdown text.
+#[derive(Debug, Clone)]
+pub struct Text {
+ spans: Vec<Span>,
+ last_style: Cell<Option<Style>>,
+ last_styled_spans: RefCell<Rc<[text::Span<'static, Url>]>>,
+}
+
+impl Text {
+ fn new(spans: Vec<Span>) -> Self {
+ Self {
+ spans,
+ last_style: Cell::default(),
+ last_styled_spans: RefCell::default(),
+ }
+ }
+
+ /// Returns the [`rich_text()`] spans ready to be used for the given style.
+ ///
+ /// This method performs caching for you. It will only reallocate if the [`Style`]
+ /// provided changes.
+ pub fn spans(&self, style: Style) -> Rc<[text::Span<'static, Url>]> {
+ if Some(style) != self.last_style.get() {
+ *self.last_styled_spans.borrow_mut() =
+ self.spans.iter().map(|span| span.view(&style)).collect();
+
+ self.last_style.set(Some(style));
+ }
+
+ self.last_styled_spans.borrow().clone()
+ }
+}
+
+#[derive(Debug, Clone)]
+enum Span {
+ Standard {
+ text: String,
+ strikethrough: bool,
+ link: Option<Url>,
+ strong: bool,
+ emphasis: bool,
+ code: bool,
+ },
+ #[cfg(feature = "highlighter")]
+ Highlight {
+ text: String,
+ color: Option<Color>,
+ font: Option<Font>,
+ },
+}
+
+impl Span {
+ fn view(&self, style: &Style) -> text::Span<'static, Url> {
+ match self {
+ Span::Standard {
+ text,
+ strikethrough,
+ link,
+ strong,
+ emphasis,
+ code,
+ } => {
+ let span = span(text.clone()).strikethrough(*strikethrough);
+
+ let span = if *code {
+ span.font(Font::MONOSPACE)
+ .color(style.inline_code_color)
+ .background(style.inline_code_highlight.background)
+ .border(style.inline_code_highlight.border)
+ .padding(style.inline_code_padding)
+ } else if *strong || *emphasis {
+ span.font(Font {
+ weight: if *strong {
+ font::Weight::Bold
+ } else {
+ font::Weight::Normal
+ },
+ style: if *emphasis {
+ font::Style::Italic
+ } else {
+ font::Style::Normal
+ },
+ ..Font::default()
+ })
+ } else {
+ span
+ };
+
+ let span = if let Some(link) = link.as_ref() {
+ span.color(style.link_color).link(link.clone())
+ } else {
+ span
+ };
+
+ span
+ }
+ #[cfg(feature = "highlighter")]
+ Span::Highlight { text, color, font } => {
+ span(text.clone()).color_maybe(*color).font_maybe(*font)
+ }
+ }
+ }
+}
+
/// Parse the given Markdown content.
-pub fn parse(
- markdown: &str,
- palette: theme::Palette,
-) -> impl Iterator<Item = Item> + '_ {
+pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
struct List {
start: Option<u64>,
items: Vec<Vec<Item>>,
@@ -158,7 +265,7 @@ pub fn parse(
pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
produce(
&mut lists,
- Item::Heading(level, spans.drain(..).collect()),
+ Item::Heading(level, Text::new(spans.drain(..).collect())),
)
}
pulldown_cmark::TagEnd::Strong if !metadata && !table => {
@@ -178,7 +285,10 @@ pub fn parse(
None
}
pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
- produce(&mut lists, Item::Paragraph(spans.drain(..).collect()))
+ produce(
+ &mut lists,
+ Item::Paragraph(Text::new(spans.drain(..).collect())),
+ )
}
pulldown_cmark::TagEnd::Item if !metadata && !table => {
if spans.is_empty() {
@@ -186,7 +296,7 @@ pub fn parse(
} else {
produce(
&mut lists,
- Item::Paragraph(spans.drain(..).collect()),
+ Item::Paragraph(Text::new(spans.drain(..).collect())),
)
}
}
@@ -207,7 +317,10 @@ pub fn parse(
highlighter = None;
}
- produce(&mut lists, Item::CodeBlock(spans.drain(..).collect()))
+ produce(
+ &mut lists,
+ Item::CodeBlock(Text::new(spans.drain(..).collect())),
+ )
}
pulldown_cmark::TagEnd::MetadataBlock(_) => {
metadata = false;
@@ -227,9 +340,11 @@ pub fn parse(
for (range, highlight) in
highlighter.highlight_line(text.as_ref())
{
- let span = span(text[range].to_owned())
- .color_maybe(highlight.color())
- .font_maybe(highlight.font());
+ let span = Span::Highlight {
+ text: text[range].to_owned(),
+ color: highlight.color(),
+ font: highlight.font(),
+ };
spans.push(span);
}
@@ -237,30 +352,13 @@ pub fn parse(
return None;
}
- let span = span(text.into_string()).strikethrough(strikethrough);
-
- let span = if strong || emphasis {
- span.font(Font {
- weight: if strong {
- font::Weight::Bold
- } else {
- font::Weight::Normal
- },
- style: if emphasis {
- font::Style::Italic
- } else {
- font::Style::Normal
- },
- ..Font::default()
- })
- } else {
- span
- };
-
- let span = if let Some(link) = link.as_ref() {
- span.color(palette.primary).link(link.clone())
- } else {
- span
+ let span = Span::Standard {
+ text: text.into_string(),
+ strong,
+ emphasis,
+ strikethrough,
+ link: link.clone(),
+ code: false,
};
spans.push(span);
@@ -268,29 +366,38 @@ pub fn parse(
None
}
pulldown_cmark::Event::Code(code) if !metadata && !table => {
- let span = span(code.into_string())
- .font(Font::MONOSPACE)
- .color(Color::WHITE)
- .background(color!(0x111111))
- .border(border::rounded(2))
- .padding(padding::left(2).right(2))
- .strikethrough(strikethrough);
-
- let span = if let Some(link) = link.as_ref() {
- span.color(palette.primary).link(link.clone())
- } else {
- span
+ let span = Span::Standard {
+ text: code.into_string(),
+ strong,
+ emphasis,
+ strikethrough,
+ link: link.clone(),
+ code: true,
};
spans.push(span);
None
}
pulldown_cmark::Event::SoftBreak if !metadata && !table => {
- spans.push(span(" ").strikethrough(strikethrough));
+ spans.push(Span::Standard {
+ text: String::from(" "),
+ strikethrough,
+ strong,
+ emphasis,
+ link: link.clone(),
+ code: false,
+ });
None
}
pulldown_cmark::Event::HardBreak if !metadata && !table => {
- spans.push(span("\n"));
+ spans.push(Span::Standard {
+ text: String::from("\n"),
+ strikethrough,
+ strong,
+ emphasis,
+ link: link.clone(),
+ code: false,
+ });
None
}
_ => None,
@@ -346,14 +453,44 @@ impl Default for Settings {
}
}
+/// The text styling of some Markdown rendering in [`view`].
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Style {
+ /// The [`Highlight`] to be applied to the background of inline code.
+ pub inline_code_highlight: Highlight,
+ /// The [`Padding`] to be applied to the background of inline code.
+ pub inline_code_padding: Padding,
+ /// The [`Color`] to be applied to inline code.
+ pub inline_code_color: Color,
+ /// The [`Color`] to be applied to links.
+ pub link_color: Color,
+}
+
+impl Style {
+ /// Creates a new [`Style`] from the given [`theme::Palette`].
+ pub fn from_palette(palette: theme::Palette) -> Self {
+ Self {
+ inline_code_padding: padding::left(1).right(1),
+ inline_code_highlight: Highlight {
+ background: color!(0x111).into(),
+ border: border::rounded(2),
+ },
+ inline_code_color: Color::WHITE,
+ link_color: palette.primary,
+ }
+ }
+}
+
/// Display a bunch of Markdown items.
///
/// You can obtain the items with [`parse`].
-pub fn view<'a, Renderer>(
+pub fn view<'a, Theme, Renderer>(
items: impl IntoIterator<Item = &'a Item>,
settings: Settings,
+ style: Style,
) -> Element<'a, Url, Theme, Renderer>
where
+ Theme: Catalog + 'a,
Renderer: core::text::Renderer<Font = Font> + 'a,
{
let Settings {
@@ -371,7 +508,7 @@ where
let blocks = items.into_iter().enumerate().map(|(i, item)| match item {
Item::Heading(level, heading) => {
- container(rich_text(heading).size(match level {
+ container(rich_text(heading.spans(style)).size(match level {
pulldown_cmark::HeadingLevel::H1 => h1_size,
pulldown_cmark::HeadingLevel::H2 => h2_size,
pulldown_cmark::HeadingLevel::H3 => h3_size,
@@ -387,11 +524,11 @@ where
.into()
}
Item::Paragraph(paragraph) => {
- rich_text(paragraph).size(text_size).into()
+ rich_text(paragraph.spans(style)).size(text_size).into()
}
Item::List { start: None, items } => {
column(items.iter().map(|items| {
- row![text("•").size(text_size), view(items, settings)]
+ row![text("•").size(text_size), view(items, settings, style)]
.spacing(spacing)
.into()
}))
@@ -404,7 +541,7 @@ where
} => column(items.iter().enumerate().map(|(i, items)| {
row![
text!("{}.", i as u64 + *start).size(text_size),
- view(items, settings)
+ view(items, settings, style)
]
.spacing(spacing)
.into()
@@ -414,7 +551,9 @@ where
Item::CodeBlock(code) => container(
scrollable(
container(
- rich_text(code).font(Font::MONOSPACE).size(code_size),
+ rich_text(code.spans(style))
+ .font(Font::MONOSPACE)
+ .size(code_size),
)
.padding(spacing.0 / 2.0),
)
@@ -426,9 +565,23 @@ where
)
.width(Length::Fill)
.padding(spacing.0 / 2.0)
- .style(container::dark)
+ .class(Theme::code_block())
.into(),
});
Element::new(column(blocks).width(Length::Fill).spacing(text_size))
}
+
+/// The theme catalog of Markdown items.
+pub trait Catalog:
+ container::Catalog + scrollable::Catalog + text::Catalog
+{
+ /// The styling class of a Markdown code block.
+ fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
+}
+
+impl Catalog for Theme {
+ fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> {
+ Box::new(container::dark)
+ }
+}
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/pane_grid.rs b/widget/src/pane_grid.rs
index 0aab1ab5..710a5443 100644
--- a/widget/src/pane_grid.rs
+++ b/widget/src/pane_grid.rs
@@ -10,6 +10,7 @@
mod axis;
mod configuration;
mod content;
+mod controls;
mod direction;
mod draggable;
mod node;
@@ -22,6 +23,7 @@ pub mod state;
pub use axis::Axis;
pub use configuration::Configuration;
pub use content::Content;
+pub use controls::Controls;
pub use direction::Direction;
pub use draggable::Draggable;
pub use node::Node;
diff --git a/widget/src/pane_grid/controls.rs b/widget/src/pane_grid/controls.rs
new file mode 100644
index 00000000..13b57acb
--- /dev/null
+++ b/widget/src/pane_grid/controls.rs
@@ -0,0 +1,59 @@
+use crate::container;
+use crate::core::{self, Element};
+
+/// The controls of a [`Pane`].
+///
+/// [`Pane`]: super::Pane
+#[allow(missing_debug_implementations)]
+pub struct Controls<
+ 'a,
+ Message,
+ Theme = crate::Theme,
+ Renderer = crate::Renderer,
+> where
+ Theme: container::Catalog,
+ Renderer: core::Renderer,
+{
+ pub(super) full: Element<'a, Message, Theme, Renderer>,
+ pub(super) compact: Option<Element<'a, Message, Theme, Renderer>>,
+}
+
+impl<'a, Message, Theme, Renderer> Controls<'a, Message, Theme, Renderer>
+where
+ Theme: container::Catalog,
+ Renderer: core::Renderer,
+{
+ /// Creates a new [`Controls`] with the given content.
+ pub fn new(
+ content: impl Into<Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ Self {
+ full: content.into(),
+ compact: None,
+ }
+ }
+
+ /// Creates a new [`Controls`] with a full and compact variant.
+ /// If there is not enough room to show the full variant without overlap,
+ /// then the compact variant will be shown instead.
+ pub fn dynamic(
+ full: impl Into<Element<'a, Message, Theme, Renderer>>,
+ compact: impl Into<Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ Self {
+ full: full.into(),
+ compact: Some(compact.into()),
+ }
+ }
+}
+
+impl<'a, Message, Theme, Renderer> From<Element<'a, Message, Theme, Renderer>>
+ for Controls<'a, Message, Theme, Renderer>
+where
+ Theme: container::Catalog,
+ Renderer: core::Renderer,
+{
+ fn from(value: Element<'a, Message, Theme, Renderer>) -> Self {
+ Self::new(value)
+ }
+}
diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs
index 791fab4a..5002b4f7 100644
--- a/widget/src/pane_grid/title_bar.rs
+++ b/widget/src/pane_grid/title_bar.rs
@@ -9,6 +9,7 @@ use crate::core::{
self, Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size,
Vector,
};
+use crate::pane_grid::controls::Controls;
/// The title bar of a [`Pane`].
///
@@ -24,7 +25,7 @@ pub struct TitleBar<
Renderer: core::Renderer,
{
content: Element<'a, Message, Theme, Renderer>,
- controls: Option<Element<'a, Message, Theme, Renderer>>,
+ controls: Option<Controls<'a, Message, Theme, Renderer>>,
padding: Padding,
always_show_controls: bool,
class: Theme::Class<'a>,
@@ -51,7 +52,7 @@ where
/// Sets the controls of the [`TitleBar`].
pub fn controls(
mut self,
- controls: impl Into<Element<'a, Message, Theme, Renderer>>,
+ controls: impl Into<Controls<'a, Message, Theme, Renderer>>,
) -> Self {
self.controls = Some(controls.into());
self
@@ -104,10 +105,22 @@ where
Renderer: core::Renderer,
{
pub(super) fn state(&self) -> Tree {
- let children = if let Some(controls) = self.controls.as_ref() {
- vec![Tree::new(&self.content), Tree::new(controls)]
- } else {
- vec![Tree::new(&self.content), Tree::empty()]
+ let children = match self.controls.as_ref() {
+ Some(controls) => match controls.compact.as_ref() {
+ Some(compact) => vec![
+ Tree::new(&self.content),
+ Tree::new(&controls.full),
+ Tree::new(compact),
+ ],
+ None => vec![
+ Tree::new(&self.content),
+ Tree::new(&controls.full),
+ Tree::empty(),
+ ],
+ },
+ None => {
+ vec![Tree::new(&self.content), Tree::empty(), Tree::empty()]
+ }
};
Tree {
@@ -117,9 +130,13 @@ where
}
pub(super) fn diff(&self, tree: &mut Tree) {
- if tree.children.len() == 2 {
+ if tree.children.len() == 3 {
if let Some(controls) = self.controls.as_ref() {
- tree.children[1].diff(controls);
+ if let Some(compact) = controls.compact.as_ref() {
+ tree.children[2].diff(compact);
+ }
+
+ tree.children[1].diff(&controls.full);
}
tree.children[0].diff(&self.content);
@@ -164,18 +181,42 @@ where
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- show_title = false;
+ if let Some(compact) = controls.compact.as_ref() {
+ let compact_layout = children.next().unwrap();
+
+ compact.as_widget().draw(
+ &tree.children[2],
+ renderer,
+ theme,
+ &inherited_style,
+ compact_layout,
+ cursor,
+ viewport,
+ );
+ } else {
+ show_title = false;
+
+ controls.full.as_widget().draw(
+ &tree.children[1],
+ renderer,
+ theme,
+ &inherited_style,
+ controls_layout,
+ cursor,
+ viewport,
+ );
+ }
+ } else {
+ controls.full.as_widget().draw(
+ &tree.children[1],
+ renderer,
+ theme,
+ &inherited_style,
+ controls_layout,
+ cursor,
+ viewport,
+ );
}
-
- controls.as_widget().draw(
- &tree.children[1],
- renderer,
- theme,
- &inherited_style,
- controls_layout,
- cursor,
- viewport,
- );
}
}
@@ -207,13 +248,20 @@ where
let mut children = padded.children();
let title_layout = children.next().unwrap();
- if self.controls.is_some() {
+ if let Some(controls) = self.controls.as_ref() {
let controls_layout = children.next().unwrap();
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- !controls_layout.bounds().contains(cursor_position)
+ if controls.compact.is_some() {
+ let compact_layout = children.next().unwrap();
+
+ !compact_layout.bounds().contains(cursor_position)
+ && !title_layout.bounds().contains(cursor_position)
+ } else {
+ !controls_layout.bounds().contains(cursor_position)
+ }
} else {
!controls_layout.bounds().contains(cursor_position)
&& !title_layout.bounds().contains(cursor_position)
@@ -244,25 +292,73 @@ where
let title_size = title_layout.size();
let node = if let Some(controls) = &self.controls {
- let controls_layout = controls.as_widget().layout(
+ let controls_layout = controls.full.as_widget().layout(
&mut tree.children[1],
renderer,
&layout::Limits::new(Size::ZERO, max_size),
);
- let controls_size = controls_layout.size();
- let space_before_controls = max_size.width - controls_size.width;
-
- let height = title_size.height.max(controls_size.height);
-
- layout::Node::with_children(
- Size::new(max_size.width, height),
- vec![
- title_layout,
- controls_layout
- .move_to(Point::new(space_before_controls, 0.0)),
- ],
- )
+ if title_layout.bounds().width + controls_layout.bounds().width
+ > max_size.width
+ {
+ if let Some(compact) = controls.compact.as_ref() {
+ let compact_layout = compact.as_widget().layout(
+ &mut tree.children[2],
+ renderer,
+ &layout::Limits::new(Size::ZERO, max_size),
+ );
+
+ let compact_size = compact_layout.size();
+ let space_before_controls =
+ max_size.width - compact_size.width;
+
+ let height = title_size.height.max(compact_size.height);
+
+ layout::Node::with_children(
+ Size::new(max_size.width, height),
+ vec![
+ title_layout,
+ controls_layout,
+ compact_layout.move_to(Point::new(
+ space_before_controls,
+ 0.0,
+ )),
+ ],
+ )
+ } else {
+ let controls_size = controls_layout.size();
+ let space_before_controls =
+ max_size.width - controls_size.width;
+
+ let height = title_size.height.max(controls_size.height);
+
+ layout::Node::with_children(
+ Size::new(max_size.width, height),
+ vec![
+ title_layout,
+ controls_layout.move_to(Point::new(
+ space_before_controls,
+ 0.0,
+ )),
+ ],
+ )
+ }
+ } else {
+ let controls_size = controls_layout.size();
+ let space_before_controls =
+ max_size.width - controls_size.width;
+
+ let height = title_size.height.max(controls_size.height);
+
+ layout::Node::with_children(
+ Size::new(max_size.width, height),
+ vec![
+ title_layout,
+ controls_layout
+ .move_to(Point::new(space_before_controls, 0.0)),
+ ],
+ )
+ }
} else {
layout::Node::with_children(
Size::new(max_size.width, title_size.height),
@@ -293,15 +389,33 @@ where
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- show_title = false;
- }
+ if let Some(compact) = controls.compact.as_ref() {
+ let compact_layout = children.next().unwrap();
- controls.as_widget().operate(
- &mut tree.children[1],
- controls_layout,
- renderer,
- operation,
- );
+ compact.as_widget().operate(
+ &mut tree.children[2],
+ compact_layout,
+ renderer,
+ operation,
+ );
+ } else {
+ show_title = false;
+
+ controls.full.as_widget().operate(
+ &mut tree.children[1],
+ controls_layout,
+ renderer,
+ operation,
+ );
+ }
+ } else {
+ controls.full.as_widget().operate(
+ &mut tree.children[1],
+ controls_layout,
+ renderer,
+ operation,
+ );
+ }
};
if show_title {
@@ -337,19 +451,45 @@ where
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- show_title = false;
- }
+ if let Some(compact) = controls.compact.as_mut() {
+ let compact_layout = children.next().unwrap();
+
+ compact.as_widget_mut().on_event(
+ &mut tree.children[2],
+ event.clone(),
+ compact_layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ )
+ } else {
+ show_title = false;
- controls.as_widget_mut().on_event(
- &mut tree.children[1],
- event.clone(),
- controls_layout,
- cursor,
- renderer,
- clipboard,
- shell,
- viewport,
- )
+ controls.full.as_widget_mut().on_event(
+ &mut tree.children[1],
+ event.clone(),
+ controls_layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ )
+ }
+ } else {
+ controls.full.as_widget_mut().on_event(
+ &mut tree.children[1],
+ event.clone(),
+ controls_layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ )
+ }
} else {
event::Status::Ignored
};
@@ -396,18 +536,33 @@ where
if let Some(controls) = &self.controls {
let controls_layout = children.next().unwrap();
- let controls_interaction = controls.as_widget().mouse_interaction(
- &tree.children[1],
- controls_layout,
- cursor,
- viewport,
- renderer,
- );
+ let controls_interaction =
+ controls.full.as_widget().mouse_interaction(
+ &tree.children[1],
+ controls_layout,
+ cursor,
+ viewport,
+ renderer,
+ );
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- controls_interaction
+ if let Some(compact) = controls.compact.as_ref() {
+ let compact_layout = children.next().unwrap();
+ let compact_interaction =
+ compact.as_widget().mouse_interaction(
+ &tree.children[2],
+ compact_layout,
+ cursor,
+ viewport,
+ renderer,
+ );
+
+ compact_interaction.max(title_interaction)
+ } else {
+ controls_interaction
+ }
} else {
controls_interaction.max(title_interaction)
}
@@ -444,12 +599,36 @@ where
controls.as_mut().and_then(|controls| {
let controls_layout = children.next()?;
- controls.as_widget_mut().overlay(
- controls_state,
- controls_layout,
- renderer,
- translation,
- )
+ if title_layout.bounds().width
+ + controls_layout.bounds().width
+ > padded.bounds().width
+ {
+ if let Some(compact) = controls.compact.as_mut() {
+ let compact_state = states.next().unwrap();
+ let compact_layout = children.next()?;
+
+ compact.as_widget_mut().overlay(
+ compact_state,
+ compact_layout,
+ renderer,
+ translation,
+ )
+ } else {
+ controls.full.as_widget_mut().overlay(
+ controls_state,
+ controls_layout,
+ renderer,
+ translation,
+ )
+ }
+ } else {
+ controls.full.as_widget_mut().overlay(
+ controls_state,
+ controls_layout,
+ renderer,
+ translation,
+ )
+ }
})
})
}
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 e586684a..aebf68e2 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;
@@ -408,10 +408,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(
@@ -422,10 +422,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(
@@ -443,7 +443,7 @@ where
},
..renderer::Quad::default()
},
- style.handle.color,
+ style.handle.background,
);
}
@@ -524,12 +524,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.
@@ -537,8 +537,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.
@@ -601,13 +601,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 c6aa1e14..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::{
@@ -13,8 +13,6 @@ use crate::core::{
Rectangle, Shell, Size, Vector, Widget,
};
-use std::borrow::Cow;
-
/// A bunch of [`Rich`] text.
#[allow(missing_debug_implementations)]
pub struct Rich<'a, Link, Theme = crate::Theme, Renderer = crate::Renderer>
@@ -23,7 +21,7 @@ where
Theme: Catalog,
Renderer: core::text::Renderer,
{
- spans: Cow<'a, [Span<'a, Link, Renderer::Font>]>,
+ spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>,
size: Option<Pixels>,
line_height: LineHeight,
width: Length,
@@ -31,6 +29,7 @@ where
font: Option<Renderer::Font>,
align_x: alignment::Horizontal,
align_y: alignment::Vertical,
+ wrapping: Wrapping,
class: Theme::Class<'a>,
}
@@ -39,11 +38,12 @@ where
Link: Clone + 'static,
Theme: Catalog,
Renderer: core::text::Renderer,
+ Renderer::Font: 'a,
{
/// Creates a new empty [`Rich`] text.
pub fn new() -> Self {
Self {
- spans: Cow::default(),
+ spans: Box::new([]),
size: None,
line_height: LineHeight::default(),
width: Length::Shrink,
@@ -51,16 +51,17 @@ where
font: None,
align_x: alignment::Horizontal::Left,
align_y: alignment::Vertical::Top,
+ wrapping: Wrapping::default(),
class: Theme::default(),
}
}
/// Creates a new [`Rich`] text with the given text spans.
pub fn with_spans(
- spans: impl Into<Cow<'a, [Span<'a, Link, Renderer::Font>]>>,
+ spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a,
) -> Self {
Self {
- spans: spans.into(),
+ spans: Box::new(spans),
..Self::new()
}
}
@@ -119,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
@@ -154,15 +161,6 @@ where
self.class = class.into();
self
}
-
- /// Adds a new text [`Span`] to the [`Rich`] text.
- pub fn push(
- mut self,
- span: impl Into<Span<'a, Link, Renderer::Font>>,
- ) -> Self {
- self.spans.to_mut().push(span.into());
- self
- }
}
impl<'a, Link, Theme, Renderer> Default for Rich<'a, Link, Theme, Renderer>
@@ -170,6 +168,7 @@ where
Link: Clone + 'a,
Theme: Catalog,
Renderer: core::text::Renderer,
+ Renderer::Font: 'a,
{
fn default() -> Self {
Self::new()
@@ -221,12 +220,13 @@ where
limits,
self.width,
self.height,
- self.spans.as_ref(),
+ self.spans.as_ref().as_ref(),
self.line_height,
self.size,
self.font,
self.align_x,
self.align_y,
+ self.wrapping,
)
}
@@ -250,7 +250,7 @@ where
.position_in(layout.bounds())
.and_then(|position| state.paragraph.hit_span(position));
- for (index, span) in self.spans.iter().enumerate() {
+ for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
let is_hovered_link =
span.link.is_some() && Some(index) == hovered_span;
@@ -394,6 +394,8 @@ where
Some(span) if span == span_pressed => {
if let Some(link) = self
.spans
+ .as_ref()
+ .as_ref()
.get(span)
.and_then(|span| span.link.clone())
{
@@ -427,7 +429,7 @@ where
if let Some(span) = state
.paragraph
.hit_span(position)
- .and_then(|span| self.spans.get(span))
+ .and_then(|span| self.spans.as_ref().as_ref().get(span))
{
if span.link.is_some() {
return mouse::Interaction::Pointer;
@@ -451,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,
@@ -471,6 +474,7 @@ where
horizontal_alignment,
vertical_alignment,
shaping: Shaping::Advanced,
+ wrapping,
};
if state.spans != spans {
@@ -487,6 +491,7 @@ where
horizontal_alignment,
vertical_alignment,
shaping: Shaping::Advanced,
+ wrapping,
}) {
core::text::Difference::None => {}
core::text::Difference::Bounds => {
@@ -509,14 +514,12 @@ where
Link: Clone + 'a,
Theme: Catalog,
Renderer: core::text::Renderer,
+ Renderer::Font: 'a,
{
fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>(
spans: T,
) -> Self {
- Self {
- spans: spans.into_iter().collect(),
- ..Self::new()
- }
+ Self::with_spans(spans.into_iter().collect::<Vec<_>>())
}
}
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 173de136..d5ede524 100644
--- a/widget/src/text_input.rs
+++ b/widget/src/text_input.rs
@@ -19,7 +19,7 @@ use crate::core::keyboard::key;
use crate::core::layout;
use crate::core::mouse::{self, click};
use crate::core::renderer;
-use crate::core::text::paragraph;
+use crate::core::text::paragraph::{self, Paragraph as _};
use crate::core::text::{self, Text};
use crate::core::time::{Duration, Instant};
use crate::core::touch;
@@ -74,6 +74,7 @@ pub struct TextInput<
padding: Padding,
size: Option<Pixels>,
line_height: text::LineHeight,
+ alignment: alignment::Horizontal,
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_submit: Option<Message>,
@@ -103,6 +104,7 @@ where
padding: DEFAULT_PADDING,
size: None,
line_height: text::LineHeight::default(),
+ alignment: alignment::Horizontal::Left,
on_input: None,
on_paste: None,
on_submit: None,
@@ -127,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
}
@@ -142,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(
@@ -152,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
@@ -193,6 +224,15 @@ where
self
}
+ /// Sets the horizontal alignment of the [`TextInput`].
+ pub fn align_x(
+ mut self,
+ alignment: impl Into<alignment::Horizontal>,
+ ) -> Self {
+ self.alignment = alignment.into();
+ self
+ }
+
/// Sets the style of the [`TextInput`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
@@ -240,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);
@@ -264,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);
@@ -384,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((
@@ -457,9 +499,21 @@ where
};
let draw = |renderer: &mut Renderer, viewport| {
+ let paragraph = if text.is_empty() {
+ state.placeholder.raw()
+ } else {
+ state.value.raw()
+ };
+
+ let alignment_offset = alignment_offset(
+ text_bounds.width,
+ paragraph.min_width(),
+ self.alignment,
+ );
+
if let Some((cursor, color)) = cursor {
renderer.with_translation(
- Vector::new(-offset, 0.0),
+ Vector::new(alignment_offset - offset, 0.0),
|renderer| {
renderer.fill_quad(cursor, color);
},
@@ -469,13 +523,9 @@ where
}
renderer.fill_paragraph(
- if text.is_empty() {
- state.placeholder.raw()
- } else {
- state.value.raw()
- },
+ paragraph,
Point::new(text_bounds.x, text_bounds.center_y())
- - Vector::new(offset, 0.0),
+ + Vector::new(alignment_offset - offset, 0.0),
if text.is_empty() {
style.placeholder
} else {
@@ -512,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;
}
}
@@ -578,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(|| {
@@ -600,10 +643,24 @@ where
if let Some(cursor_position) = click_position {
let text_layout = layout.children().next().unwrap();
- let target = cursor_position.x - text_layout.bounds().x;
- let click =
- mouse::Click::new(cursor_position, state.last_click);
+ let target = {
+ let text_bounds = text_layout.bounds();
+
+ let alignment_offset = alignment_offset(
+ text_bounds.width,
+ state.value.raw().min_width(),
+ self.alignment,
+ );
+
+ cursor_position.x - text_bounds.x - alignment_offset
+ };
+
+ let click = mouse::Click::new(
+ cursor_position,
+ mouse::Button::Left,
+ state.last_click,
+ );
match click.kind() {
click::Kind::Single => {
@@ -677,7 +734,18 @@ where
if state.is_dragging {
let text_layout = layout.children().next().unwrap();
- let target = position.x - text_layout.bounds().x;
+
+ let target = {
+ let text_bounds = text_layout.bounds();
+
+ let alignment_offset = alignment_offset(
+ text_bounds.width,
+ state.value.raw().min_width(),
+ self.alignment,
+ );
+
+ position.x - text_bounds.x - alignment_offset
+ };
let value = if self.is_secure {
self.value.secure()
@@ -706,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();
@@ -733,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)
{
@@ -757,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 => {
@@ -800,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) =
@@ -828,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()
{
@@ -852,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()
{
@@ -1070,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
}
@@ -1382,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(),
});
}
@@ -1486,3 +1571,21 @@ pub fn default(theme: &Theme, status: Status) -> Style {
},
}
}
+
+fn alignment_offset(
+ text_bounds_width: f32,
+ text_min_width: f32,
+ alignment: alignment::Horizontal,
+) -> f32 {
+ if text_min_width > text_bounds_width {
+ 0.0
+ } else {
+ match alignment {
+ alignment::Horizontal::Left => 0.0,
+ alignment::Horizontal::Center => {
+ (text_bounds_width - text_min_width) / 2.0
+ }
+ alignment::Horizontal::Right => text_bounds_width - text_min_width,
+ }
+ }
+}
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 f21b996c..03ec374c 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};
@@ -413,10 +413,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(
@@ -427,10 +427,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(
@@ -448,7 +448,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 e762ae7c..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,
@@ -513,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
@@ -842,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 c5c3133d..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),
@@ -1291,7 +1303,7 @@ fn run_action<P, C>(
}
}
window::Action::GetPosition(id, channel) => {
- if let Some(window) = window_manager.get_mut(id) {
+ if let Some(window) = window_manager.get(id) {
let position = window
.raw
.inner_position()
@@ -1306,6 +1318,13 @@ fn run_action<P, C>(
let _ = channel.send(position);
}
}
+ window::Action::GetScaleFactor(id, channel) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ let scale_factor = window.raw.scale_factor();
+
+ let _ = channel.send(scale_factor as f32);
+ }
+ }
window::Action::Move(id, position) => {
if let Some(window) = window_manager.get_mut(id) {
window.raw.set_outer_position(
@@ -1416,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 fcbf79f6..3d22e155 100644
--- a/winit/src/program/window_manager.rs
+++ b/winit/src/program/window_manager.rs
@@ -74,12 +74,20 @@ 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>)> {
self.entries.iter_mut().map(|(k, v)| (*k, v))
}
+ pub fn get(&self, id: Id) -> Option<&Window<P, C>> {
+ self.entries.get(&id)
+ }
+
pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<P, C>> {
self.entries.get_mut(&id)
}