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