feat: pop-os megasquash
x11: Workaround nvidia driver lacking DRI
feat(mouse area): add double click
mouse area: add double click
compositor: Add code to extract adapter from x11
refactor: Extract ids_from_dev from wayland specific code
wayland: Don't crash if libwayland isn't available
feat(sctk): support for overflow widget
sctk: Fixes for cursor icon
* With multiple windows, `SetCursor` is only sent for the focused
window. Fixing a flicker between icons when two windows are using
different cursors.
* If there is a drag surface, let that surface set the cursor. And not
any other.
* Set cursor on `enter`, and when switching between CSDs and app area.
Fixes https://github.com/pop-os/libcosmic/issues/533.
improv(sctk): per-surface cursor position tracking
feat(sctk): support ShowWindowMenu
Make text wrap configurable
fix(core): state order and handling of new trees
fix: settings.decorations enables SSD
refactor(sctk): convert window actions
fix: enable the tokio feature for accesskit_unix
fix: only try to connect to clipboard if on linux
iced_wgpu: don't query Wayland on macos
Update `window_clipboard`
sctk: Unmap subsurfaces instead of immediately destroying them
Destroying a surface is immediate, rather than synchronized with
commits.
This fixes a flickering behavior with drag and drop in
cosmic-workspaces.
sctk: Add alpha setting to `Subsurface` widget
sctk: Update `sctk`, `wayland-protocols`
Update for cosmic-text undefined buffer size
Adapt to cosmic-text undefined width change
fix: unset VK_LOADER_DRIVERS_DISABLE after enumeration
Allows applications to be launched on the NVIDIA GPU with Vulkan support
Adapt to new cosmic-text
wgpu: Fix wayland device id conversion
wgpu: Fix querying adapter, even if we already have one
wgpu: fix nvidia gpu powering up in hybrid setups
cargo fmt
fix: refactor dnd impl to support responsive widget
fix: update read and write methods so they don't recurse
fix(core): replace debug_assert in diff
fix: avoid with_borrow_mut
fix: better handling of state tree
This persists widget state associated with widgets assigned custom IDs even when the tree structure changes, but resets state if the custom ID is not found.
fix: emit Event::Resized to fix nav bar in cosmic-settings
fix(image): guess the image format before decoding
iced_wgpu: Query wayland for the device to use, if possible
sctk: Support `start_drag` with drags started from touch events
sctk: Add touch support
fix: update widnow-clipboard tag
fix: clean up dnd surfaces when a window is removed
Adjust to line ending needing to be specified as part of cosmic_text::BufferLine
sctk: Add support for drag-and-drop surface offsets
This adds an offset `Vector` as an argument to `on_drag`, and allows
passing an offset to `start_drag`.
Some applications using drag and drop want the top left corner of the
drag surface (as happens without an offset). But others want the drag
surface to be offset based on where the cursor is on the widget when
starting the drag. This can just be `-1 * offset`, but may be scaled if
the drag surface is a different size from the original widget.
fix(sctk): nested popup parent
feat(mouseare): mouse enter and exit
fix(tiny_skia): damage
fix(scrollable): filter scroll events in the wrong direction
sctk: Use empty input region for subsurfaces
This seems to work, and is a better way to deal with subsurface input if
there aren't any problems. This way, input events simply go to the
parent surface, so we don't have to deal with various edge cases related
to that. (Though for compositor-side issues, we still need to fix those
for other clients.)
This helps with an issue with drag-and-drop and subsurfaces on Smithay,
and a different issue on Kwin (in KDE 5.27, at least).
Send `DataSource` events to all surfaces
Previously these events are directed to the first surface, then removed
from `sctk_events`. Which is definitely not right.
slider & toggler roundness
Update window_clipboard to pop-dnd-4
fix(tiny-skia): non-simple border scaling
the issue can be seen with sharp corners when using the screenshot portal with scaling
Add read_primary/write_primary
chore: update tag
fix: translate offer positions in scrollable
fix(winit multi-window): handle exit_on_close request
fix(scrollable): pass child layout when calculating drag destinations
fix(container): id and set_id should use content
Clean up after lock surfaces are destroyed
Call unlock on session lock
chore: update tag
fixes for dnd
sctk: Fix handling of DnD with subsurfaces (#122)
Map subsurface to parent and add offset.
refactor: remove Sync bound for Message
fix: pass correct state and layout for container widgets
fix: docs
feat: update advertised drag destinations after rebuilding an interface
fix: color format & multi-window
fix: doc
feat: winit dnd
fix: ambiguous import
chore: reexport mime from window_clipboard
chore: use tag
clippy
feat: add actions and commands for new clipboard methods
cleanup docs
feat: custom mime types for Clipboard
sctk: Fix handling of layer surface `pointer_interactivity` (#115)
A null `region` represents an infinite region (the default). To set an
empty region, we need to create a `wl_region`.
fix(tiny_skia): disable shadows due to rendering glitch
fix(winit): add static lifetimes to multi-window application update
fix(winit): add static lifetimes to application update
Use `TypeId` to identify `subscription::Map`
(cherry picked from commit f39a5fd895)
fix(sctk): destroy drag icon and send event after cancel action
fix: clipboard cleanup
fix(sctk): clipboard dummy impl typo
refactor(sctk): optional clipboard
fix(sctk): broadcast events after update
when broadcasting events for no specific surface, it should be done after update so that the runtime subscription is current
fix(multi_window): enable drag resize
sctk: Map subsurface pointer events to parent surface, with offset
sctk_subsurface: Use two surfaces, handle button presses
Useful for testing pointer input to subsurfaces.
sctk: Add `subsurface_ids` mapping subsurface to parent and offset
sctk_subsurface_gst: NV12 surface suppport; disabled
Whether or not this works seems to depend on driver, or gstreamer
version...
Handle frame callbacks for subsurfaces, and `commit` parent surface
If the main surface is occluded completely by opaque subsurfaces, it may
not receive `frame` events. So we need to request frame events for all
subsurfaces as well.
Additionally, with "synchronized" subsurfaces, we need to `commit` the
parent surface for subsurface changes to take effect.
Fixes issues with subsurfaces updating slowly, or only when mouse moved
under some circumstances.
examples/sctk_subsurface_gst: Cache `BufferSource` in `BufferRef` qdata
Similar to `waylandsink`. Allows us to avoid creating a buffer source
(and ultimately `wl_buffer`) for every buffer swap.
sctk/subsurface: Cache `wl_buffer`s
Creating a new `wl_buffer` each frame seems to perform poorly. We can
instead keep a cache of `wl_buffer`s we have created from a
`BufferSource`.
sctk/subsurface: Avoid unnecessary subsurface commits if unchanged
feat(slider): add breakpoints
fix: autosize surface layout
Autosized surfaces perform the layout step to get the size and then again when building the interface, but sometimes the calculated size is not enough space when used as a bound, so we need to add a tiny amount to the calculated size. This also makes the event loop timeout duration configurable. Viewport physical size is calculated directly from the logical size now as well in iced-sctk to avoid inconsistencies that resulted from recalculating the logical size after using it to calculate the physical size.
fix(sctk): send close event instead of close requested when a window is closed
sctk: add command to set maximize state
Add `show_window_menu` action
Winit currently supports this only on Windows and Wayland.
This requests that a context menu is shown at the cursor position,
like the menu normally triggered by right clicking the title bar. This
is important for implementing client side decorations with Iced widgets.
Remove unnecessary redraw request
This was particularly visible on Redox where there is no vsync, but also
causes unnecessary redraws on Linux
chore: update accesskit
Disable broken rustdoc links
sctk: Add `Subsurface` widget (#79)
This adds a widget that attaches an shm or dma buffer to a subsurface,
scaled with `wp_viewporter`.
By exposing this as a widget, rather than as a type of window, it can be
positioned and scaled like any other iced widget. It provides an API
that's similar to an iced image.
The initial version of this just took a `wl_buffer`. But this makes
buffer re-use problematic. In particular, the docs for `wl_surface::attach`
note that `wl_buffer::release` events become unreliable if a buffer is
attached to multiple surfaces. And indicates that a client should create
multiple `wl_buffer` instances, or use `wp_linux_buffer_release`.
So we store information about the buffer, and create `wl_buffer`s as
needed. `SubsurfaceBuffer::new` also returns a future that's signaled
when all references are destroyed, both `wl_buffer`s and any instance of
the `SubsurfaceBuffer` that might still be used in the `view`.
So this seems like the best solution for now, within the
model-view-update architecture.
This has two examples: `sctk_subsurface`, showing a single-color shm
buffer, and `sctk_subsurface_gst`, which plays an h264 video to a
subsurface with vaapi decoding.
chore: use pop-os fork of winit
chore: unpin cosmic-text
Update wgpu to a commit that fixes use on Nvidia drivers
This can be tested with something like
`VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/nvidia_icd.json cargo run -p tour
--features iced/wgpu`.
On Nvidia I'm seeing a flood of `Suboptimal present of frame` warnings.
So some improvement may still be needed here. But if it doesn't regress
behavior on other hardware, that seems like an improvement over
freezing.
fix(winit): pass text with modifiers in event
chore: update cosmic-text and glyphon
fix: distinguish between the key character and the utf8 of a key event
feat(wgpu): use alpha modes for compositing if available
chore: use updated softbuffer
fix: typo
fix: downgrade resvg
fix: core/serde
chore: remove default features
typo: add rev to glyphon
Update to cosmic-text refactor
Fix docs error
Add function to fill a Raw
Fixes for last commit
fix: broadcast surface events
dnd_listener: Fix behavior when there are multiple listeners (#87)
A `dnd_listener` widget shouldn't handle a DnD event when the dnd drag
isn't within the widget's bounds. So add a few more checks for this.
Enter/leave events generated by `DndOfferEvent::Motion` also don't
behave as one might expect, since the enter may occur before the leave
depending on the order it calls `on_event` on the widget. Not sure how
to address that, but cosmic-workspaces can just ignore the leave events
for now.
Otherwise, this seems to be working fine, after these changes.
chore: fix sctk multi-window dependency
cleanup: formatting and clippy
fix(example): sctk_drag id
fix: translate the wayland event position for content inside a scrollable
fix: set web-sys to =0.3.64
fix: clip mask checks
chore: use advanced text shaping for pick list
fix: dnd widget layout
fix: ambiguous palette import
chore: remove artifacts job
fix: CI tests
fix: add back the window id to the frames subscription
fix: tooltip children and diff
refactor: udpate gradient angles for slider
reexport limits
fix: editor and sctk_todos examples
cleanup: clippy
cleanup git workflows
chore: cleanup iced_widget
refactor
Update mod.rs
chore: update softbuffer
Hack to remove image blur
iced_core: feature for serde serialization of KeyCode
fix(wgpu): handle border_radius property with image raster
feat: add border radius to image rendering
feat: Add side mouse button events
cleanup: clippy fixes and formatting
Part of this is a refactor of the ID
cleanup: clippy and fmt
fix: test workflow
fix: add note in CHANGELOG
fix: clippy
refactor: restore default style of slider
feat: allow setting the width and height of a rule
fix: slider gradient angle
feat: gradient backgground for the slider rail
feat(mouse-area): added on_drag method
fix(widget): container inherited wrong icon color from renderer
fix(button): inherit icon color if set to none
feat(renderer): define default icon color
By default, this is the same as the text color for best visibility.
feat(winit): client-side resize drag support
feat(winit): client-side resize drag support
Make vertical scroll properties optional
fix: quad rendering including border only inside of the bounds
Move `Screenshot` inside `window` module
Added offscreen rendering support for wgpu & tiny-skia exposed with the window::screenshot command.
Provide access to font from each crate
Use nested for lazy widgets
Use layout with children for nesting
Introduce internal `overlay::Nested` for `UserInterface`
fix: reset button state if the cursor leaves
runtime: Handle widget operations in `program::State` helper (#46)
chore: default line height, text size, and shaping for cosmic
feat: sctk shell
fix: quad rendering including border only inside of the bounds
fix: better slider drawing (it allows just the border part of the handle quad outside of the layout bouds, which isn't great, but is ok for our purposes due to being transparent)
cleanup: fix & format
fix: use iced_core::Font
cleanup
fix: allow leaving out winit & iced-sctk
fix: settings
fix: slider draw improvements
fix: websocket example
fix: modal example
fix: scrollable example
fix: toast example
fix: avoid panicking in iced_sctk with lazy widgets in auto-size surfaces
fix: todos panic
fix: only diff auto-sized surfaces in iced_sctk build_user_interface & improve sctk examples
wip (iced-sctk): window resize with icons
feat (iced-sctk): support for setting cursor
refactor: default decorations to client
fix: set window geometry after receiving configure
fix: size limits with no max bound must be cut off
fix: send size update when autosized surface resizes
fix: use ceil size for positioner
cleanup: remove dbg statement
fix: remove a destroyed surface from compositor surfaces
fix errors after rebase and wip scaling support
fix: handling of scale factor in set_logical_size
fix (sctk_drag example): add .into for border radius
fix: fractional scaling
sctk: Fire RedrawRequests
wip: animations via frame event
fix / refactor iced-sctk redraw & frame event handling
cleanup: note about frame request in iced-sctk
fix: send resize when necessary for layer surface and popups too
fix: always request redraw for a new surface
fix: scaling and autosize surface improvements
refactor: sctk_lazy keyboard interactivity
feat(sctk): configurable natural_scroll property
feat: send state and capabilities events when there are changes
fix: redraw when an update is needed and clean up the logic
Update sctk to latest commit
Fix compilation of sctk drag example
fix(sctk): update interface before checking if it has a redraw request
refactor: after autosize surface resize wait to redraw until the resize has been applied
refactor: better handling of autosize surfaces
chore: update sctk
chore: update sctk
fixes sctk_drag example
fix: default to ControlFlow::Wait for applications with no surface
this seems to help CPU usage for app library and launcher
default to 250ms timeout in the event loop
Update sctk
sctk: Implement xdg-activation support
fix: don't require Flags to be clone for settings on wayland
chore: error if neither winit or wayland feature is set
chore: Allow compiling without windowing system (#65)
fix(iced-sctk): handle exit_on_close_request
fix: make sure that each widget operation operates on every interface
This should be ok even for widget actions like focus next because there can only ever be a single focused widget
cargo fmt
cleanup: dbg statement
fix(iced-sctk): replace panic with handling for remaining enum variants
refactor: use iced clipboard for interacting with the selection
refactor: allow passing an activation token when creating a window
sctk: Add support for `ext-session-lock` protocol
fix(sctk): build and use tree for layout of autosize surfaces
Update winit to latest commit used by upstream iced
fix(sctk): send key characters
fix(sctk): check if key is a named key first
refactor(sctk): keep compositor surface in state
feat: accessibility with some widget impls
feat: stable ids
a11y: Don't unconditionally pull winit (#43)
Update conversion.rs
integration fixes
integration
integration
integration
integration
s
integration
some integration work
more integration
Update Cargo.toml
Update mod.rs
Update multi_window.rs
s
more integration
more integration (ryanabx wip #100000)
more integration!!
integration 2
integration
more integration (rbx)
s
integration
integration work
integration
Update Cargo.toml
integration
simple integration things
int
integration to 175
integration(170)
Co-Authored-By: Ashley Wulber <48420062+wash2@users.noreply.github.com>
Co-Authored-By: Victoria Brekenfeld <4404502+Drakulix@users.noreply.github.com>
Co-Authored-By: Eduardo Flores <edfloreshz@gmail.com>
Co-Authored-By: Michael Murphy <michael@mmurphy.dev>
Co-Authored-By: wiiznokes <78230769+wiiznokes@users.noreply.github.com>
Co-Authored-By: Jeremy Soller <jeremy@system76.com>
Co-Authored-By: Ryan Brue <56272643+ryanabx@users.noreply.github.com>
fix(column): handle keys len change
fix: iced-sctk a11y
wip: winit single window updates
tokio feature hangs even without a11y feature
fix: multiwindow a11y fixes
fix: component
update winit
wip: sctk integration to winit shell
refactor: remove accesskit_unix
fix: svg
fix: remove wayland default feature
feat: derive Hash for image Handle
fix: cleanup 0.13 rebase errors
fix: remove path dependencies
conversion for Radius
conversion for Padding
setter for Svg border radius
re-export Limits
fix: connect clipboard if disconnected on layer surface or popup creation
fix: connect clipboard if disconnected on session lock surface creation
fix: update size of layer surface after configure
fix: insert user interfaces for popup and lock surfaces on creation
fix: svg scaling
feat: popups on winit windows
fix: default text shaping to advanced
fix: fallback to renderer icon style if svg is symbolic
fixes
fix: sctk frame handling
feat: autosize handling
fix: better autosize handling
fix: avoid duplicate window events from sctk
fix: better handling of popups
fix: refactor redraw handling for sctk
fix: include id in frames
fix: image
fix: scrollable delta direction
sctk: unregister clipboard when surface is done
set min / max size when size is requested
fix: popups
filter pointer events
feat: add Hide variant to mouse Interaction
dnd fixes
fix: use physical width for DnD surface
fix: tiny-skia svg quality
refactor: peek_dnd try to parse data
cleanup text conversion
cleanup
svg scaling fixes
background color fix
Introduce consecutive_click_distance like other toolkits do such as gtk,qt,imgui.
This commit is contained in:
parent
595af03a9f
commit
08fe1f3aa5
233 changed files with 24391 additions and 1911 deletions
12
.github/workflows/audit.yml
vendored
12
.github/workflows/audit.yml
vendored
|
|
@ -16,15 +16,3 @@ jobs:
|
||||||
run: cargo update
|
run: cargo update
|
||||||
- name: Audit vulnerabilities
|
- name: Audit vulnerabilities
|
||||||
run: cargo audit
|
run: cargo audit
|
||||||
|
|
||||||
# artifacts:
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# steps:
|
|
||||||
# - uses: hecrj/setup-rust-action@v2
|
|
||||||
# - name: Install cargo-outdated
|
|
||||||
# run: cargo install cargo-outdated
|
|
||||||
# - uses: actions/checkout@master
|
|
||||||
# - name: Delete `web-sys` dependency from `integration` example
|
|
||||||
# run: sed -i '$d' examples/integration/Cargo.toml
|
|
||||||
# - name: Find outdated dependencies
|
|
||||||
# run: cargo outdated --workspace --exit-code 1 --ignore raw-window-handle
|
|
||||||
|
|
|
||||||
7
.github/workflows/lint.yml
vendored
7
.github/workflows/lint.yml
vendored
|
|
@ -7,11 +7,14 @@ jobs:
|
||||||
- uses: hecrj/setup-rust-action@v2
|
- uses: hecrj/setup-rust-action@v2
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
|
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
export DEBIAN_FRONTED=noninteractive
|
export DEBIAN_FRONTED=noninteractive
|
||||||
sudo apt-get -qq update
|
sudo apt-get -qq update
|
||||||
sudo apt-get install -y libxkbcommon-dev libgtk-3-dev
|
sudo apt-get install -y libxkbcommon-dev libgtk-3-dev libwayland-dev
|
||||||
- name: Check lints
|
- name: Check lints
|
||||||
run: cargo lint
|
run: |
|
||||||
|
cargo clippy --no-default-features --features "winit" --all-targets
|
||||||
|
cargo clippy --no-default-features --features "wayland wgpu svg canvas qr_code lazy debug tokio palette web-colors a11y"
|
||||||
|
|
|
||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
export DEBIAN_FRONTED=noninteractive
|
export DEBIAN_FRONTED=noninteractive
|
||||||
sudo apt-get -qq update
|
sudo apt-get -qq update
|
||||||
sudo apt-get install -y libxkbcommon-dev libgtk-3-dev
|
sudo apt-get install -y libxkbcommon-dev libwayland-dev
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
cargo test --verbose --workspace
|
cargo test --verbose --workspace
|
||||||
|
|
|
||||||
|
|
@ -601,6 +601,8 @@ Many thanks to...
|
||||||
- @wiiznokes
|
- @wiiznokes
|
||||||
- @woelfman
|
- @woelfman
|
||||||
- @Zaubentrucker
|
- @Zaubentrucker
|
||||||
|
- @ryanabx
|
||||||
|
- @edfloreshz
|
||||||
|
|
||||||
## [0.12.1] - 2024-02-22
|
## [0.12.1] - 2024-02-22
|
||||||
### Added
|
### Added
|
||||||
|
|
@ -787,6 +789,10 @@ Many thanks to...
|
||||||
- @william-shere
|
- @william-shere
|
||||||
- @wyatt-herkamp
|
- @wyatt-herkamp
|
||||||
|
|
||||||
|
Many thanks to...
|
||||||
|
- @jackpot51
|
||||||
|
- @wash2
|
||||||
|
|
||||||
## [0.10.0] - 2023-07-28
|
## [0.10.0] - 2023-07-28
|
||||||
### Added
|
### Added
|
||||||
- Text shaping, font fallback, and `iced_wgpu` overhaul. [#1697](https://github.com/iced-rs/iced/pull/1697)
|
- Text shaping, font fallback, and `iced_wgpu` overhaul. [#1697](https://github.com/iced-rs/iced/pull/1697)
|
||||||
|
|
|
||||||
1826
Cargo.lock
generated
1826
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
156
Cargo.toml
156
Cargo.toml
|
|
@ -22,12 +22,14 @@ all-features = true
|
||||||
maintenance = { status = "actively-developed" }
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["wgpu", "tiny-skia", "crisp", "web-colors", "thread-pool", "linux-theme-detection", "x11", "wayland"]
|
|
||||||
# Enables the `wgpu` GPU-accelerated renderer with all its default features (Vulkan, Metal, DX12, OpenGL, and WebGPU)
|
|
||||||
wgpu = ["wgpu-bare", "iced_renderer/wgpu"]
|
wgpu = ["wgpu-bare", "iced_renderer/wgpu"]
|
||||||
# Enables the `wgpu` GPU-accelerated renderer with the minimum required features (no backends!)
|
# Enables the `wgpu` GPU-accelerated renderer with the minimum required features (no backends!)
|
||||||
wgpu-bare = ["iced_renderer/wgpu-bare", "iced_widget/wgpu"]
|
wgpu-bare = ["iced_renderer/wgpu-bare", "iced_widget/wgpu"]
|
||||||
# Enables the `tiny-skia` software renderer
|
# Enables the `tiny-skia` software renderer
|
||||||
|
|
||||||
|
|
||||||
|
default = ["tiny-skia", "x11", "wayland", "tokio"]
|
||||||
|
# Enable the `tiny-skia` software renderer backend
|
||||||
tiny-skia = ["iced_renderer/tiny-skia"]
|
tiny-skia = ["iced_renderer/tiny-skia"]
|
||||||
# Enables the `image` widget
|
# Enables the `image` widget
|
||||||
image = ["image-without-codecs", "image/default"]
|
image = ["image-without-codecs", "image/default"]
|
||||||
|
|
@ -39,11 +41,11 @@ svg = ["iced_widget/svg"]
|
||||||
canvas = ["iced_widget/canvas"]
|
canvas = ["iced_widget/canvas"]
|
||||||
# Enables the `qr_code` widget
|
# Enables the `qr_code` widget
|
||||||
qr_code = ["iced_widget/qr_code"]
|
qr_code = ["iced_widget/qr_code"]
|
||||||
# Enables the `markdown` widget
|
|
||||||
markdown = ["iced_widget/markdown"]
|
|
||||||
# Enables lazy widgets
|
|
||||||
lazy = ["iced_widget/lazy"]
|
|
||||||
# Enables debug metrics in native platforms (press F12)
|
# Enables debug metrics in native platforms (press F12)
|
||||||
|
lazy = ["iced_widget/lazy"] # Enables lazy widgets
|
||||||
|
markdown = ["iced_widget/markdown"] # Enables the `markdown` widget
|
||||||
debug = ["iced_winit/debug", "dep:iced_devtools"]
|
debug = ["iced_winit/debug", "dep:iced_devtools"]
|
||||||
# Enables time-travel debugging (very experimental!)
|
# Enables time-travel debugging (very experimental!)
|
||||||
time-travel = ["debug", "iced_devtools/time-travel"]
|
time-travel = ["debug", "iced_devtools/time-travel"]
|
||||||
|
|
@ -51,10 +53,15 @@ time-travel = ["debug", "iced_devtools/time-travel"]
|
||||||
hot = ["debug", "iced_debug/hot"]
|
hot = ["debug", "iced_debug/hot"]
|
||||||
# Enables the tester developer tool for recording and playing tests (press F12)
|
# Enables the tester developer tool for recording and playing tests (press F12)
|
||||||
tester = ["dep:iced_tester"]
|
tester = ["dep:iced_tester"]
|
||||||
|
|
||||||
|
|
||||||
# Enables the `thread-pool` futures executor as the `executor::Default` on native platforms
|
# Enables the `thread-pool` futures executor as the `executor::Default` on native platforms
|
||||||
thread-pool = ["iced_futures/thread-pool"]
|
thread-pool = ["iced_futures/thread-pool"]
|
||||||
|
|
||||||
# Enables `tokio` as the `executor::Default` on native platforms
|
# Enables `tokio` as the `executor::Default` on native platforms
|
||||||
tokio = ["iced_futures/tokio"]
|
tokio = ["iced_futures/tokio", "iced_accessibility?/tokio"]
|
||||||
|
# Enables `async-std` as the `executor::Default` on native platforms
|
||||||
|
async-std = ["iced_accessibility?/async-io"]
|
||||||
# Enables `smol` as the `executor::Default` on native platforms
|
# Enables `smol` as the `executor::Default` on native platforms
|
||||||
smol = ["iced_futures/smol"]
|
smol = ["iced_futures/smol"]
|
||||||
# Enables querying system information
|
# Enables querying system information
|
||||||
|
|
@ -62,18 +69,30 @@ sysinfo = ["iced_winit/sysinfo"]
|
||||||
# Enables broken "sRGB linear" blending to reproduce color management of the Web
|
# Enables broken "sRGB linear" blending to reproduce color management of the Web
|
||||||
web-colors = ["iced_renderer/web-colors"]
|
web-colors = ["iced_renderer/web-colors"]
|
||||||
# Enables pixel snapping for crisp edges by default (can cause jitter!)
|
# Enables pixel snapping for crisp edges by default (can cause jitter!)
|
||||||
|
|
||||||
|
|
||||||
|
# Enables the `widget::selector` module
|
||||||
crisp = ["iced_core/crisp", "iced_widget/crisp"]
|
crisp = ["iced_core/crisp", "iced_widget/crisp"]
|
||||||
# Enables the WebGL backend
|
# Enables the WebGL backend
|
||||||
|
|
||||||
|
|
||||||
webgl = ["iced_renderer/webgl"]
|
webgl = ["iced_renderer/webgl"]
|
||||||
# Enables syntax highlighting
|
# Enables syntax highlighting
|
||||||
highlighter = ["iced_highlighter", "iced_widget/highlighter"]
|
highlighter = ["iced_highlighter", "iced_widget/highlighter"]
|
||||||
# Enables the `widget::selector` module
|
|
||||||
selector = ["iced_runtime/selector"]
|
selector = ["iced_runtime/selector"]
|
||||||
# Enables the advanced module
|
|
||||||
advanced = ["iced_core/advanced", "iced_widget/advanced"]
|
|
||||||
# Embeds Fira Sans into the final application; useful for testing and Wasm builds
|
|
||||||
fira-sans = ["iced_renderer/fira-sans"]
|
|
||||||
# Enables basic text shaping by default
|
# Enables basic text shaping by default
|
||||||
|
fira-sans = [
|
||||||
|
"iced_renderer/fira-sans",
|
||||||
|
] # Embeds Fira Sans into the final application; useful for testing and Wasm builds
|
||||||
|
advanced = [
|
||||||
|
"iced_core/advanced",
|
||||||
|
"iced_widget/advanced",
|
||||||
|
] # Enables the advanced module
|
||||||
|
multi-window = [
|
||||||
|
"iced_winit?/multi-window",
|
||||||
|
] # Enables experimental multi-window support.
|
||||||
basic-shaping = ["iced_core/basic-shaping"]
|
basic-shaping = ["iced_core/basic-shaping"]
|
||||||
# Enables advanced text shaping by default
|
# Enables advanced text shaping by default
|
||||||
advanced-shaping = ["iced_core/advanced-shaping"]
|
advanced-shaping = ["iced_core/advanced-shaping"]
|
||||||
|
|
@ -83,32 +102,48 @@ strict-assertions = ["iced_renderer/strict-assertions"]
|
||||||
unconditional-rendering = ["iced_winit/unconditional-rendering"]
|
unconditional-rendering = ["iced_winit/unconditional-rendering"]
|
||||||
# Enables support for the `sipper` library
|
# Enables support for the `sipper` library
|
||||||
sipper = ["iced_runtime/sipper"]
|
sipper = ["iced_runtime/sipper"]
|
||||||
|
|
||||||
|
|
||||||
# Enables Linux system theme detection
|
# Enables Linux system theme detection
|
||||||
linux-theme-detection = ["iced_winit/linux-theme-detection"]
|
linux-theme-detection = ["iced_winit/linux-theme-detection"]
|
||||||
# Enables the Unix X11 backend
|
# Enables the Unix X11 backend
|
||||||
x11 = ["iced_renderer/x11", "iced_winit/x11"]
|
x11 = ["iced_renderer/x11", "iced_winit/x11"]
|
||||||
# Enables the Unix Wayland backend
|
# Enables the `accesskit` accessibility library
|
||||||
wayland = ["iced_renderer/wayland", "iced_winit/wayland"]
|
a11y = [
|
||||||
|
"iced_accessibility",
|
||||||
|
"iced_core/a11y",
|
||||||
|
"iced_widget/a11y",
|
||||||
|
"iced_winit?/a11y",
|
||||||
|
]
|
||||||
|
# Enables the winit shell. Conflicts with `wayland` and `glutin`.
|
||||||
|
winit = ["iced_winit", "iced_accessibility?/accesskit_winit"]
|
||||||
|
# Enables the sctk shell.
|
||||||
|
wayland = ["iced_widget/wayland", "iced_core/wayland", "iced_winit/wayland"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
|
|
||||||
|
iced_devtools.workspace = true
|
||||||
iced_debug.workspace = true
|
iced_debug.workspace = true
|
||||||
|
|
||||||
|
iced_program.workspace = true
|
||||||
iced_core.workspace = true
|
iced_core.workspace = true
|
||||||
iced_futures.workspace = true
|
iced_futures.workspace = true
|
||||||
iced_renderer.workspace = true
|
iced_renderer.workspace = true
|
||||||
iced_runtime.workspace = true
|
iced_runtime.workspace = true
|
||||||
iced_widget.workspace = true
|
iced_widget.workspace = true
|
||||||
iced_winit.workspace = true
|
iced_winit.workspace = true
|
||||||
|
|
||||||
iced_devtools.workspace = true
|
|
||||||
iced_devtools.optional = true
|
iced_devtools.optional = true
|
||||||
|
|
||||||
iced_tester.workspace = true
|
iced_tester.workspace = true
|
||||||
iced_tester.optional = true
|
iced_tester.optional = true
|
||||||
|
iced_winit.optional = true
|
||||||
iced_highlighter.workspace = true
|
iced_highlighter.workspace = true
|
||||||
iced_highlighter.optional = true
|
iced_highlighter.optional = true
|
||||||
|
iced_accessibility.workspace = true
|
||||||
|
iced_accessibility.optional = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
window_clipboard.workspace = true
|
||||||
|
mime.workspace = true
|
||||||
|
dnd.workspace = true
|
||||||
|
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
image.optional = true
|
image.optional = true
|
||||||
|
|
@ -152,7 +187,9 @@ members = [
|
||||||
"widget",
|
"widget",
|
||||||
"winit",
|
"winit",
|
||||||
"examples/*",
|
"examples/*",
|
||||||
|
"accessibility",
|
||||||
]
|
]
|
||||||
|
exclude = ["examples/integration"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
|
@ -184,16 +221,28 @@ iced_tiny_skia = { version = "0.14.0", path = "tiny_skia", default-features = fa
|
||||||
iced_wgpu = { version = "0.14.0", path = "wgpu", default-features = false }
|
iced_wgpu = { version = "0.14.0", path = "wgpu", default-features = false }
|
||||||
iced_widget = { version = "0.14.0", path = "widget" }
|
iced_widget = { version = "0.14.0", path = "widget" }
|
||||||
iced_winit = { version = "0.14.0", path = "winit", default-features = false }
|
iced_winit = { version = "0.14.0", path = "winit", default-features = false }
|
||||||
|
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
bitflags = "2.0"
|
cargo-hot = { version = "0.1", package = "cargo-hot-protocol" }
|
||||||
|
futures = { version = "0.3", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"async-await",
|
||||||
|
] }
|
||||||
|
glam = "0.25"
|
||||||
|
iced_accessibility = { version = "0.1", path = "accessibility" }
|
||||||
|
|
||||||
|
async-std = "1.0"
|
||||||
|
|
||||||
|
|
||||||
|
bitflags = "2.5"
|
||||||
bytemuck = { version = "1.0", features = ["derive"] }
|
bytemuck = { version = "1.0", features = ["derive"] }
|
||||||
bytes = "1.6"
|
bytes = "1.6"
|
||||||
cargo-hot = { version = "0.1", package = "cargo-hot-protocol" }
|
cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git" }
|
||||||
cosmic-text = "0.15"
|
# cosmic-text = "0.10"
|
||||||
cryoglyph = "0.1"
|
dark-light = "1.0"
|
||||||
futures = { version = "0.3", default-features = false, features = ["std", "async-await"] }
|
cryoglyph = { package = "cryoglyph", git = "https://github.com/pop-os/glyphon.git", branch = "iced-0.14" }
|
||||||
glam = "0.25"
|
resvg = "0.45"
|
||||||
|
# glyphon = { package = "iced_glyphon", path = "../../../glyphon" }
|
||||||
|
web-sys = "0.3.69"
|
||||||
guillotiere = "0.6"
|
guillotiere = "0.6"
|
||||||
half = "2.2"
|
half = "2.2"
|
||||||
image = { version = "0.25", default-features = false }
|
image = { version = "0.25", default-features = false }
|
||||||
|
|
@ -211,7 +260,6 @@ png = "0.18"
|
||||||
pulldown-cmark = "0.12"
|
pulldown-cmark = "0.12"
|
||||||
qrcode = { version = "0.13", default-features = false }
|
qrcode = { version = "0.13", default-features = false }
|
||||||
raw-window-handle = "0.6"
|
raw-window-handle = "0.6"
|
||||||
resvg = "0.45"
|
|
||||||
rfd = "0.16"
|
rfd = "0.16"
|
||||||
rustc-hash = "2.0"
|
rustc-hash = "2.0"
|
||||||
semver = "1.0"
|
semver = "1.0"
|
||||||
|
|
@ -219,28 +267,56 @@ serde = "1.0"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
sipper = "0.1"
|
sipper = "0.1"
|
||||||
smol = "2"
|
smol = "2"
|
||||||
smol_str = "0.2"
|
smol_str = "0.3"
|
||||||
softbuffer = { version = "0.4", default-features = false }
|
|
||||||
sysinfo = "0.33"
|
sysinfo = "0.33"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tiny-skia = { version = "0.11", default-features = false, features = ["std", "simd"] }
|
tiny-skia = { version = "0.11", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"simd",
|
||||||
|
] }
|
||||||
|
sctk = { package = "smithay-client-toolkit", version = "0.19.1" }
|
||||||
|
softbuffer = { git = "https://github.com/pop-os/softbuffer", tag = "cosmic-4.0" }
|
||||||
|
syntect = "5.2"
|
||||||
tokio = "1.0"
|
tokio = "1.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
two-face = { version = "0.4", default-features = false, features = ["syntect-default-fancy"] }
|
two-face = { version = "0.4", default-features = false, features = [
|
||||||
|
"syntect-default-fancy",
|
||||||
|
] }
|
||||||
unicode-segmentation = "1.0"
|
unicode-segmentation = "1.0"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
wasmtimer = "0.4.2"
|
wasmtimer = "0.4.2"
|
||||||
web-sys = "0.3.69"
|
|
||||||
web-time = "1.1"
|
web-time = "1.1"
|
||||||
wgpu = { version = "27.0", default-features = false, features = ["std", "wgsl"] }
|
wgpu = { version = "27.0", default-features = false, features = [
|
||||||
window_clipboard = { version = "0.5", default-features = false }
|
"std",
|
||||||
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
|
"wgsl",
|
||||||
|
] }
|
||||||
|
wayland-protocols = { version = "0.32.1", features = ["staging"] }
|
||||||
|
# web-time = "1.1"
|
||||||
|
|
||||||
|
|
||||||
|
# wgpu = "0.19"
|
||||||
|
# Newer wgpu commit that fixes Vulkan backend on Nvidia
|
||||||
|
winapi = "0.3"
|
||||||
|
# window_clipboard = "0.4.1"
|
||||||
|
|
||||||
|
window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-0.13-2" }
|
||||||
|
dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-0.13-2" }
|
||||||
|
mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-0.13-2" }
|
||||||
|
# window_clipboard = { path = "../../window_clipboard", tag = "pop-0.13-2" }
|
||||||
|
# dnd = { path = "../../window_clipboard/dnd", tag = "pop-0.13" }
|
||||||
|
# mime = { path = "../../window_clipboard/mime", tag = "pop-0.13" }
|
||||||
|
# winit = { git = "https://github.com/pop-os/winit.git", tag = "iced-xdg-surface-0.13" }
|
||||||
|
winit = { path = "../../winit/winit" }
|
||||||
|
winit-core = { path = "../../winit/winit-core" }
|
||||||
|
cursor-icon = "1.1.0"
|
||||||
|
# winit = { git = "https://github.com/iced-rs/winit.git", rev = "254d6b3420ce4e674f516f7a2bd440665e05484d" }
|
||||||
|
# winit = { git = "https://github.com/rust-windowing/winit.git", rev = "241b7a80bba96c91fa3901729cd5dec66abb9be4" }
|
||||||
|
# winit = { path = "../../../winit" }
|
||||||
|
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
rust_2018_idioms = { level = "deny", priority = -1 }
|
rust_2018_idioms = { level = "deny", priority = -1 }
|
||||||
missing_docs = "deny"
|
|
||||||
unsafe_code = "deny"
|
|
||||||
unused_results = "deny"
|
unused_results = "deny"
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
|
|
@ -263,3 +339,7 @@ useless_conversion = "deny"
|
||||||
|
|
||||||
[workspace.lints.rustdoc]
|
[workspace.lints.rustdoc]
|
||||||
broken_intra_doc_links = "forbid"
|
broken_intra_doc_links = "forbid"
|
||||||
|
|
||||||
|
# [patch."https://github.com/rust-windowing/winit.git"]
|
||||||
|
# winit = { git = "https://github.com/rust-windowing/winit.git", rev = "241b7a80bba96c91fa3901729cd5dec66abb9be4" }
|
||||||
|
# winit = { path = "../../../winit" }
|
||||||
|
|
|
||||||
24
accessibility/Cargo.toml
Normal file
24
accessibility/Cargo.toml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "iced_accessibility"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
# TODO Ashley re-export more platform adapters
|
||||||
|
[features]
|
||||||
|
async-io = ["accesskit_winit?/async-io"]
|
||||||
|
tokio = ["accesskit_winit?/tokio"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# accesskit = { git = "https://github.com/wash2/accesskit", tag = "iced-xdg-surface-0.13" }
|
||||||
|
# accesskit_windows = { git = "https://github.com/wash2/accesskit", tag = "iced-xdg-surface-0.13", optional = true }
|
||||||
|
# accesskit_macos = { git = "https://github.com/wash2/accesskit", tag = "iced-xdg-surface-0.13", optional = true }
|
||||||
|
# accesskit_winit = { git = "https://github.com/wash2/accesskit", tag = "iced-xdg-surface-0.13", optional = true, default-features = false, features = [
|
||||||
|
# "rwh_06",
|
||||||
|
# ] }
|
||||||
|
accesskit = { path = "../../../accesskit/common" }
|
||||||
|
accesskit_windows = { path = "../../../accesskit/platforms/windows", optional = true }
|
||||||
|
accesskit_macos = { path = "../../../accesskit/platforms/macos", optional = true }
|
||||||
|
accesskit_winit = { path = "../../../accesskit/platforms/winit", optional = true, default-features = false, features = [
|
||||||
|
"rwh_06",
|
||||||
|
] }
|
||||||
80
accessibility/src/a11y_tree.rs
Normal file
80
accessibility/src/a11y_tree.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
use crate::{A11yId, A11yNode, IdEq};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
/// Accessible tree of nodes
|
||||||
|
pub struct A11yTree {
|
||||||
|
/// The root of the current widget, children of the parent widget or the Window if there is no parent widget
|
||||||
|
root: Vec<A11yNode>,
|
||||||
|
/// The children of a widget and its children
|
||||||
|
children: Vec<A11yNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl A11yTree {
|
||||||
|
/// Create a new A11yTree
|
||||||
|
/// XXX if you use this method, you will need to manually add the children of the root nodes
|
||||||
|
pub fn new(root: Vec<A11yNode>, children: Vec<A11yNode>) -> Self {
|
||||||
|
Self { root, children }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leaf<T: Into<A11yId>>(node: accesskit::Node, id: T) -> Self {
|
||||||
|
Self {
|
||||||
|
root: vec![A11yNode::new(node, id)],
|
||||||
|
children: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper for creating an A11y tree with a single root node and some children
|
||||||
|
pub fn node_with_child_tree(mut root: A11yNode, child_tree: Self) -> Self {
|
||||||
|
root.add_children(
|
||||||
|
child_tree.root.iter().map(|n| n.id()).cloned().collect(),
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
root: vec![root],
|
||||||
|
children: child_tree
|
||||||
|
.children
|
||||||
|
.into_iter()
|
||||||
|
.chain(child_tree.root)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Joins multiple trees into a single tree
|
||||||
|
pub fn join<T: Iterator<Item = Self>>(trees: T) -> Self {
|
||||||
|
trees.fold(Self::default(), |mut acc, A11yTree { root, children }| {
|
||||||
|
acc.root.extend(root);
|
||||||
|
acc.children.extend(children);
|
||||||
|
acc
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn root(&self) -> &Vec<A11yNode> {
|
||||||
|
&self.root
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn children(&self) -> &Vec<A11yNode> {
|
||||||
|
&self.children
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn root_mut(&mut self) -> &mut Vec<A11yNode> {
|
||||||
|
&mut self.root
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn children_mut(&mut self) -> &mut Vec<A11yNode> {
|
||||||
|
&mut self.children
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&self, id: &A11yId) -> bool {
|
||||||
|
self.root.iter().any(|n| IdEq::eq(n.id(), id))
|
||||||
|
|| self.children.iter().any(|n| IdEq::eq(n.id(), id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<A11yTree> for Vec<(accesskit::NodeId, accesskit::Node)> {
|
||||||
|
fn from(tree: A11yTree) -> Vec<(accesskit::NodeId, accesskit::Node)> {
|
||||||
|
tree.root
|
||||||
|
.into_iter()
|
||||||
|
.map(|node| node.into())
|
||||||
|
.chain(tree.children.into_iter().map(|node| node.into()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
228
accessibility/src/id.rs
Normal file
228
accessibility/src/id.rs
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
//! Widget and Window IDs.
|
||||||
|
|
||||||
|
use std::borrow::{self, Cow};
|
||||||
|
use std::hash::Hash;
|
||||||
|
use std::sync::atomic::{self, AtomicU64};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum A11yId {
|
||||||
|
Window(u64),
|
||||||
|
Widget(Id),
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl A11yId {
|
||||||
|
// pub fn new_widget() -> Self {
|
||||||
|
// Self::Widget(Id::unique())
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn new_window() -> Self {
|
||||||
|
// Self::Window(window_node_id())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl From<u64> for A11yId {
|
||||||
|
fn from(id: u64) -> Self {
|
||||||
|
Self::Window(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Id> for A11yId {
|
||||||
|
fn from(id: Id) -> Self {
|
||||||
|
assert!(!matches!(id.0, Internal::Set(_)));
|
||||||
|
Self::Widget(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdEq for A11yId {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(A11yId::Widget(self_), A11yId::Widget(other)) => {
|
||||||
|
IdEq::eq(self_, other)
|
||||||
|
}
|
||||||
|
_ => self == other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<accesskit::NodeId> for A11yId {
|
||||||
|
fn from(value: accesskit::NodeId) -> Self {
|
||||||
|
let val = u64::from(value.0);
|
||||||
|
if val > u32::MAX as u64 {
|
||||||
|
Self::Window(value.0)
|
||||||
|
} else {
|
||||||
|
Self::Widget(Id::from(val as u64))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<A11yId> for accesskit::NodeId {
|
||||||
|
fn from(value: A11yId) -> Self {
|
||||||
|
let node_id = match value {
|
||||||
|
A11yId::Window(id) => id,
|
||||||
|
A11yId::Widget(id) => id.into(),
|
||||||
|
};
|
||||||
|
accesskit::NodeId(node_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
|
||||||
|
static NEXT_WINDOW_ID: AtomicU64 = AtomicU64::new(1);
|
||||||
|
|
||||||
|
/// The identifier of a generic widget.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Id(pub Internal);
|
||||||
|
|
||||||
|
impl Id {
|
||||||
|
/// Creates a custom [`Id`].
|
||||||
|
pub fn new(id: impl Into<borrow::Cow<'static, str>>) -> Self {
|
||||||
|
Self(Internal::Custom(Self::next(), id.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// resets the id counter
|
||||||
|
pub fn reset() {
|
||||||
|
NEXT_ID.store(1, atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next() -> u64 {
|
||||||
|
NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a unique [`Id`].
|
||||||
|
///
|
||||||
|
/// This function produces a different [`Id`] every time it is called.
|
||||||
|
pub fn unique() -> Self {
|
||||||
|
let id = Self::next();
|
||||||
|
|
||||||
|
Self(Internal::Unique(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdEq for Id {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
IdEq::eq(&self.0, &other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for Id {
|
||||||
|
fn from(value: &'static str) -> Self {
|
||||||
|
Id(Internal::Custom(Id::next(), Cow::Borrowed(value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<String> for Id {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Id(Internal::Custom(Id::next(), Cow::Owned(value.to_string())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not meant to be used directly
|
||||||
|
impl From<u64> for Id {
|
||||||
|
fn from(value: u64) -> Self {
|
||||||
|
Self(Internal::Unique(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not meant to be used directly
|
||||||
|
impl From<Id> for u64 {
|
||||||
|
fn from(val: Id) -> u64 {
|
||||||
|
match &val.0 {
|
||||||
|
Internal::Unique(id) => *id,
|
||||||
|
Internal::Custom(id, _) => *id,
|
||||||
|
// this is a set id, which is not a valid id and will not ever be converted to a NonZeroU128
|
||||||
|
// so we panic
|
||||||
|
Internal::Set(_) => {
|
||||||
|
panic!("Cannot convert a set id to a NonZeroU128")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for Id {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match &self.0 {
|
||||||
|
Internal::Unique(_) => "Undefined".to_string(),
|
||||||
|
Internal::Custom(_, id) => id.to_string(),
|
||||||
|
Internal::Set(_) => "Set".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX WIndow IDs are made unique by adding u32::MAX to them
|
||||||
|
/// get window node id that won't conflict with other node ids for the duration of the program
|
||||||
|
pub fn window_node_id() -> u64 {
|
||||||
|
u32::MAX as u64
|
||||||
|
+ NEXT_WINDOW_ID.fetch_add(1, atomic::Ordering::Relaxed) as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO refactor to make panic impossible?
|
||||||
|
#[derive(Debug, Clone, Eq)]
|
||||||
|
/// Internal representation of an [`Id`].
|
||||||
|
pub enum Internal {
|
||||||
|
/// a unique id
|
||||||
|
Unique(u64),
|
||||||
|
/// a custom id, which is equal to any [`Id`] with a matching number or string
|
||||||
|
Custom(u64, borrow::Cow<'static, str>),
|
||||||
|
/// XXX Do not use this as an id for an accessibility node, it will panic!
|
||||||
|
/// XXX Only meant to be used for widgets that have multiple accessibility nodes, each with a
|
||||||
|
/// unique or custom id
|
||||||
|
/// an Id Set, which is equal to any [`Id`] with a matching number or string
|
||||||
|
Set(Vec<Self>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Internal {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::Unique(l0), Self::Unique(r0)) => l0 == r0,
|
||||||
|
(Self::Custom(_, l1), Self::Custom(_, r1)) => l1 == r1,
|
||||||
|
(Self::Set(l0), Self::Set(r0)) => l0 == r0,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Similar to PartialEq, but only intended for use when comparing Ids
|
||||||
|
pub trait IdEq {
|
||||||
|
fn eq(&self, other: &Self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdEq for Internal {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::Unique(l0), Self::Unique(r0)) => l0 == r0,
|
||||||
|
(Self::Custom(l0, l1), Self::Custom(r0, r1)) => {
|
||||||
|
l0 == r0 || l1 == r1
|
||||||
|
}
|
||||||
|
// allow custom ids to be equal to unique ids
|
||||||
|
(Self::Unique(l0), Self::Custom(r0, _))
|
||||||
|
| (Self::Custom(l0, _), Self::Unique(r0)) => l0 == r0,
|
||||||
|
(Self::Set(l0), Self::Set(r0)) => l0 == r0,
|
||||||
|
// allow set ids to just be equal to any of their members
|
||||||
|
(Self::Set(l0), r) | (r, Self::Set(l0)) => {
|
||||||
|
l0.iter().any(|l| l == r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for Internal {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
match self {
|
||||||
|
Self::Unique(id) => id.hash(state),
|
||||||
|
Self::Custom(name, _) => name.hash(state),
|
||||||
|
Self::Set(ids) => ids.hash(state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Id;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unique_generates_different_ids() {
|
||||||
|
let a = Id::unique();
|
||||||
|
let b = Id::unique();
|
||||||
|
|
||||||
|
assert_ne!(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
accessibility/src/lib.rs
Normal file
17
accessibility/src/lib.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
mod a11y_tree;
|
||||||
|
pub mod id;
|
||||||
|
mod node;
|
||||||
|
mod traits;
|
||||||
|
|
||||||
|
pub use a11y_tree::*;
|
||||||
|
pub use accesskit;
|
||||||
|
pub use id::*;
|
||||||
|
pub use node::*;
|
||||||
|
pub use traits::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit_macos")]
|
||||||
|
pub use accesskit_macos;
|
||||||
|
#[cfg(feature = "accesskit_windows")]
|
||||||
|
pub use accesskit_windows;
|
||||||
|
#[cfg(feature = "accesskit_winit")]
|
||||||
|
pub use accesskit_winit;
|
||||||
41
accessibility/src/node.rs
Normal file
41
accessibility/src/node.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
use crate::A11yId;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct A11yNode {
|
||||||
|
node: accesskit::Node,
|
||||||
|
id: A11yId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl A11yNode {
|
||||||
|
pub fn new<T: Into<A11yId>>(node: accesskit::Node, id: T) -> Self {
|
||||||
|
Self {
|
||||||
|
node,
|
||||||
|
id: id.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> &A11yId {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn node_mut(&mut self) -> &mut accesskit::Node {
|
||||||
|
&mut self.node
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn node(&self) -> &accesskit::Node {
|
||||||
|
&self.node
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_children(&mut self, children: Vec<A11yId>) {
|
||||||
|
let mut children =
|
||||||
|
children.into_iter().map(|id| id.into()).collect::<Vec<_>>();
|
||||||
|
children.extend_from_slice(self.node.children());
|
||||||
|
self.node.set_children(children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<A11yNode> for (accesskit::NodeId, accesskit::Node) {
|
||||||
|
fn from(node: A11yNode) -> Self {
|
||||||
|
(node.id.into(), node.node)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
accessibility/src/traits.rs
Normal file
19
accessibility/src/traits.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::A11yId;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Description<'a> {
|
||||||
|
Text(Cow<'a, str>),
|
||||||
|
Id(Vec<A11yId>),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Describes a widget
|
||||||
|
pub trait Describes {
|
||||||
|
fn description(&self) -> Vec<A11yId>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Labels a widget
|
||||||
|
pub trait Labels {
|
||||||
|
fn label(&self) -> Vec<A11yId>;
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,8 @@ advanced = []
|
||||||
crisp = []
|
crisp = []
|
||||||
basic-shaping = []
|
basic-shaping = []
|
||||||
advanced-shaping = []
|
advanced-shaping = []
|
||||||
|
a11y = ["iced_accessibility"]
|
||||||
|
wayland = ["sctk"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bitflags.workspace = true
|
bitflags.workspace = true
|
||||||
|
|
@ -34,3 +36,22 @@ web-time.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde.optional = true
|
serde.optional = true
|
||||||
serde.features = ["derive"]
|
serde.features = ["derive"]
|
||||||
|
# TODO(POP): I think some of these dependencies were removed. Check on that
|
||||||
|
# xxhash-rust.workspace = true
|
||||||
|
window_clipboard.workspace = true
|
||||||
|
dnd.workspace = true
|
||||||
|
mime.workspace = true
|
||||||
|
|
||||||
|
sctk.workspace = true
|
||||||
|
sctk.optional = true
|
||||||
|
# /TODO(POP)
|
||||||
|
[dependencies.iced_accessibility]
|
||||||
|
version = "0.1.0"
|
||||||
|
path = "../accessibility"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
raw-window-handle.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
approx = "0.5"
|
||||||
|
|
|
||||||
|
|
@ -276,3 +276,54 @@ impl std::ops::Mul<f32> for Radius {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<[f32; 4]> for Radius {
|
||||||
|
/// [
|
||||||
|
/// radi.top_left,
|
||||||
|
/// radi.top_right,
|
||||||
|
/// radi.bottom_right,
|
||||||
|
/// radi.bottom_left,
|
||||||
|
/// ]
|
||||||
|
fn from(value: [f32; 4]) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: value[0],
|
||||||
|
top_right: value[1],
|
||||||
|
bottom_right: value[2],
|
||||||
|
bottom_left: value[3],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[u8; 4]> for Radius {
|
||||||
|
/// [
|
||||||
|
/// radi.top_left,
|
||||||
|
/// radi.top_right,
|
||||||
|
/// radi.bottom_right,
|
||||||
|
/// radi.bottom_left,
|
||||||
|
/// ]
|
||||||
|
fn from(value: [u8; 4]) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: f32::from(value[0]),
|
||||||
|
top_right: f32::from(value[1]),
|
||||||
|
bottom_right: f32::from(value[2]),
|
||||||
|
bottom_left: f32::from(value[3]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[u16; 4]> for Radius {
|
||||||
|
/// [
|
||||||
|
/// radi.top_left,
|
||||||
|
/// radi.top_right,
|
||||||
|
/// radi.bottom_right,
|
||||||
|
/// radi.bottom_left,
|
||||||
|
/// ]
|
||||||
|
fn from(value: [u16; 4]) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: f32::from(value[0]),
|
||||||
|
top_right: f32::from(value[1]),
|
||||||
|
bottom_right: f32::from(value[2]),
|
||||||
|
bottom_left: f32::from(value[3]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,61 @@
|
||||||
//! Access the clipboard.
|
//! Access the clipboard.
|
||||||
|
|
||||||
|
use std::{any::Any, sync::Arc};
|
||||||
|
|
||||||
|
use dnd::{DndAction, DndDestinationRectangle, DndSurface};
|
||||||
|
use mime::{self, AllowedMimeTypes, AsMimeTypes, ClipboardStoreData};
|
||||||
|
|
||||||
|
use crate::{Element, Vector, widget::tree::State, window};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct IconSurface<E> {
|
||||||
|
pub element: E,
|
||||||
|
pub state: State,
|
||||||
|
pub offset: Vector,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type DynIconSurface = IconSurface<Box<dyn Any>>;
|
||||||
|
|
||||||
|
impl<T: 'static, R: 'static> IconSurface<Element<'static, (), T, R>> {
|
||||||
|
pub fn new(
|
||||||
|
element: Element<'static, (), T, R>,
|
||||||
|
state: State,
|
||||||
|
offset: Vector,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
element,
|
||||||
|
state,
|
||||||
|
offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upcast(self) -> DynIconSurface {
|
||||||
|
IconSurface {
|
||||||
|
element: Box::new(self.element),
|
||||||
|
state: self.state,
|
||||||
|
offset: self.offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynIconSurface {
|
||||||
|
/// Downcast `element` to concrete type `Element<(), T, R>`
|
||||||
|
///
|
||||||
|
/// Panics if type doesn't match
|
||||||
|
pub fn downcast<T: 'static, R: 'static>(
|
||||||
|
self,
|
||||||
|
) -> IconSurface<Element<'static, (), T, R>> {
|
||||||
|
IconSurface {
|
||||||
|
element: *self
|
||||||
|
.element
|
||||||
|
.downcast()
|
||||||
|
.expect("drag-and-drop icon surface has invalid element type"),
|
||||||
|
state: self.state,
|
||||||
|
offset: self.offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A buffer for short-term storage and transfer within and between
|
/// A buffer for short-term storage and transfer within and between
|
||||||
/// applications.
|
/// applications.
|
||||||
pub trait Clipboard {
|
pub trait Clipboard {
|
||||||
|
|
@ -8,6 +64,62 @@ pub trait Clipboard {
|
||||||
|
|
||||||
/// Writes the given text contents to the [`Clipboard`].
|
/// Writes the given text contents to the [`Clipboard`].
|
||||||
fn write(&mut self, kind: Kind, contents: String);
|
fn write(&mut self, kind: Kind, contents: String);
|
||||||
|
|
||||||
|
/// Consider using [`read_data`] instead
|
||||||
|
/// Reads the current content of the [`Clipboard`] as text.
|
||||||
|
fn read_data(
|
||||||
|
&self,
|
||||||
|
kind: Kind,
|
||||||
|
_mimes: Vec<String>,
|
||||||
|
) -> Option<(Vec<u8>, String)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the given contents to the [`Clipboard`].
|
||||||
|
fn write_data(
|
||||||
|
&mut self,
|
||||||
|
kind: Kind,
|
||||||
|
_contents: ClipboardStoreData<
|
||||||
|
Box<dyn Send + Sync + 'static + mime::AsMimeTypes>,
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts a DnD operation.
|
||||||
|
fn register_dnd_destination(
|
||||||
|
&self,
|
||||||
|
_surface: DndSurface,
|
||||||
|
_rectangles: Vec<DndDestinationRectangle>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the final action for the DnD operation.
|
||||||
|
/// Only should be done if it is requested.
|
||||||
|
fn set_action(&self, _action: DndAction) {}
|
||||||
|
|
||||||
|
/// Registers Dnd destinations
|
||||||
|
fn start_dnd(
|
||||||
|
&mut self,
|
||||||
|
_internal: bool,
|
||||||
|
_source_surface: Option<DndSource>,
|
||||||
|
_icon_surface: Option<DynIconSurface>,
|
||||||
|
_content: Box<dyn AsMimeTypes + Send + 'static>,
|
||||||
|
_actions: DndAction,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ends a DnD operation.
|
||||||
|
fn end_dnd(&self) {}
|
||||||
|
|
||||||
|
/// Consider using [`peek_dnd`] instead
|
||||||
|
/// Peeks the data on the DnD with a specific mime type.
|
||||||
|
/// Will return an error if there is no ongoing DnD operation.
|
||||||
|
fn peek_dnd(&self, _mime: String) -> Option<(Vec<u8>, String)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request window size
|
||||||
|
fn request_logical_window_size(&self, width: f32, height: f32) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The kind of [`Clipboard`].
|
/// The kind of [`Clipboard`].
|
||||||
|
|
@ -21,6 +133,25 @@ pub enum Kind {
|
||||||
Primary,
|
Primary,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Starts a DnD operation.
|
||||||
|
/// icon surface is a tuple of the icon element and optionally the icon element state.
|
||||||
|
pub fn start_dnd<T: 'static, R: 'static>(
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
internal: bool,
|
||||||
|
source_surface: Option<DndSource>,
|
||||||
|
icon_surface: Option<IconSurface<Element<'static, (), T, R>>>,
|
||||||
|
content: Box<dyn AsMimeTypes + Send + 'static>,
|
||||||
|
actions: DndAction,
|
||||||
|
) {
|
||||||
|
clipboard.start_dnd(
|
||||||
|
internal,
|
||||||
|
source_surface,
|
||||||
|
icon_surface.map(IconSurface::upcast),
|
||||||
|
content,
|
||||||
|
actions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// A null implementation of the [`Clipboard`] trait.
|
/// A null implementation of the [`Clipboard`] trait.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct Null;
|
pub struct Null;
|
||||||
|
|
@ -32,3 +163,90 @@ impl Clipboard for Null {
|
||||||
|
|
||||||
fn write(&mut self, _kind: Kind, _contents: String) {}
|
fn write(&mut self, _kind: Kind, _contents: String) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads the current content of the [`Clipboard`].
|
||||||
|
pub fn read_data<T: AllowedMimeTypes>(
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
) -> Option<T> {
|
||||||
|
clipboard
|
||||||
|
.read_data(Kind::Standard, T::allowed().into())
|
||||||
|
.and_then(|data| T::try_from(data).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the current content of the primary [`Clipboard`].
|
||||||
|
pub fn read_primary_data<T: AllowedMimeTypes>(
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
) -> Option<T> {
|
||||||
|
clipboard
|
||||||
|
.read_data(Kind::Primary, T::allowed().into())
|
||||||
|
.and_then(|data| T::try_from(data).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the current content of the primary [`Clipboard`].
|
||||||
|
pub fn peek_dnd<T: AllowedMimeTypes>(
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
mime: Option<String>,
|
||||||
|
) -> Option<T> {
|
||||||
|
let Some(mime) = mime.or_else(|| T::allowed().first().cloned().into())
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
clipboard
|
||||||
|
.peek_dnd(mime)
|
||||||
|
.and_then(|data| T::try_from(data).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Source of a DnD operation.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum DndSource {
|
||||||
|
/// A widget is the source of the DnD operation.
|
||||||
|
Widget(crate::id::Id),
|
||||||
|
/// A surface is the source of the DnD operation.
|
||||||
|
Surface(window::Id),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list of DnD destination rectangles.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DndDestinationRectangles {
|
||||||
|
/// The rectangle of the DnD destination.
|
||||||
|
rectangles: Vec<DndDestinationRectangle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DndDestinationRectangles {
|
||||||
|
/// Creates a new [`DndDestinationRectangles`].
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
rectangles: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`DndDestinationRectangles`] with the given capacity.
|
||||||
|
pub fn with_capacity(capacity: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
rectangles: Vec::with_capacity(capacity),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes a new rectangle to the list of DnD destination rectangles.
|
||||||
|
pub fn push(&mut self, rectangle: DndDestinationRectangle) {
|
||||||
|
self.rectangles.push(rectangle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends the list of DnD destination rectangles to the current list.
|
||||||
|
pub fn append(&mut self, other: &mut Vec<DndDestinationRectangle>) {
|
||||||
|
self.rectangles.append(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the list of DnD destination rectangles.
|
||||||
|
/// This consumes the [`DndDestinationRectangles`].
|
||||||
|
pub fn into_rectangles(mut self) -> Vec<DndDestinationRectangle> {
|
||||||
|
self.rectangles.reverse();
|
||||||
|
self.rectangles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<[DndDestinationRectangle]> for DndDestinationRectangles {
|
||||||
|
fn as_ref(&self) -> &[DndDestinationRectangle] {
|
||||||
|
&self.rectangles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::event::{self, Event};
|
||||||
|
use crate::id::Id;
|
||||||
use crate::layout;
|
use crate::layout;
|
||||||
use crate::mouse;
|
use crate::mouse;
|
||||||
use crate::overlay;
|
use crate::overlay;
|
||||||
|
|
@ -5,11 +7,11 @@ use crate::renderer;
|
||||||
use crate::widget;
|
use crate::widget;
|
||||||
use crate::widget::tree::{self, Tree};
|
use crate::widget::tree::{self, Tree};
|
||||||
use crate::{
|
use crate::{
|
||||||
Border, Clipboard, Color, Event, Layout, Length, Rectangle, Shell, Size,
|
Border, Clipboard, Color, Layout, Length, Rectangle, Shell, Size, Vector,
|
||||||
Vector, Widget,
|
Widget,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::borrow::Borrow;
|
use std::borrow::{Borrow, BorrowMut};
|
||||||
|
|
||||||
/// A generic [`Widget`].
|
/// A generic [`Widget`].
|
||||||
///
|
///
|
||||||
|
|
@ -238,6 +240,37 @@ impl<'a, Message, Theme, Renderer>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer>
|
||||||
|
Borrow<dyn Widget<Message, Theme, Renderer> + 'a>
|
||||||
|
for &mut Element<'a, Message, Theme, Renderer>
|
||||||
|
{
|
||||||
|
fn borrow(&self) -> &(dyn Widget<Message, Theme, Renderer> + 'a) {
|
||||||
|
self.widget.borrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer>
|
||||||
|
BorrowMut<dyn Widget<Message, Theme, Renderer> + 'a>
|
||||||
|
for &mut Element<'a, Message, Theme, Renderer>
|
||||||
|
{
|
||||||
|
fn borrow_mut(
|
||||||
|
&mut self,
|
||||||
|
) -> &mut (dyn Widget<Message, Theme, Renderer> + 'a) {
|
||||||
|
self.widget.borrow_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer>
|
||||||
|
BorrowMut<dyn Widget<Message, Theme, Renderer> + 'a>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
{
|
||||||
|
fn borrow_mut(
|
||||||
|
&mut self,
|
||||||
|
) -> &mut (dyn Widget<Message, Theme, Renderer> + 'a) {
|
||||||
|
self.widget.borrow_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Map<'a, A, B, Theme, Renderer> {
|
struct Map<'a, A, B, Theme, Renderer> {
|
||||||
widget: Box<dyn Widget<A, Theme, Renderer> + 'a>,
|
widget: Box<dyn Widget<A, Theme, Renderer> + 'a>,
|
||||||
mapper: Box<dyn Fn(A) -> B + 'a>,
|
mapper: Box<dyn Fn(A) -> B + 'a>,
|
||||||
|
|
@ -277,8 +310,8 @@ where
|
||||||
self.widget.children()
|
self.widget.children()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff(&self, tree: &mut Tree) {
|
fn diff(&mut self, tree: &mut Tree) {
|
||||||
self.widget.diff(tree);
|
self.widget.diff(tree)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size(&self) -> Size<Length> {
|
fn size(&self) -> Size<Length> {
|
||||||
|
|
@ -376,6 +409,35 @@ where
|
||||||
.overlay(tree, layout, renderer, viewport, translation)
|
.overlay(tree, layout, renderer, viewport, translation)
|
||||||
.map(move |overlay| overlay.map(mapper))
|
.map(move |overlay| overlay.map(mapper))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "a11y")]
|
||||||
|
fn a11y_nodes(
|
||||||
|
&self,
|
||||||
|
_layout: Layout<'_>,
|
||||||
|
_state: &Tree,
|
||||||
|
_cursor_position: mouse::Cursor,
|
||||||
|
) -> iced_accessibility::A11yTree {
|
||||||
|
self.widget.a11y_nodes(_layout, _state, _cursor_position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<Id> {
|
||||||
|
self.widget.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_id(&mut self, id: Id) {
|
||||||
|
self.widget.set_id(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drag_destinations(
|
||||||
|
&self,
|
||||||
|
state: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles,
|
||||||
|
) {
|
||||||
|
self.widget
|
||||||
|
.drag_destinations(state, layout, renderer, dnd_rectangles);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Explain<'a, Message, Theme, Renderer: crate::Renderer> {
|
struct Explain<'a, Message, Theme, Renderer: crate::Renderer> {
|
||||||
|
|
@ -420,7 +482,7 @@ where
|
||||||
self.element.widget.children()
|
self.element.widget.children()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff(&self, tree: &mut Tree) {
|
fn diff(&mut self, tree: &mut Tree) {
|
||||||
self.element.widget.diff(tree);
|
self.element.widget.diff(tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -532,8 +594,31 @@ where
|
||||||
translation,
|
translation,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
fn id(&self) -> Option<Id> {
|
||||||
|
self.element.widget.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_id(&mut self, id: Id) {
|
||||||
|
self.element.widget.set_id(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drag_destinations(
|
||||||
|
&self,
|
||||||
|
state: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles,
|
||||||
|
) {
|
||||||
|
self.element.widget.drag_destinations(
|
||||||
|
state,
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
dnd_rectangles,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// TODO maybe a11y_nodes
|
||||||
|
}
|
||||||
impl<'a, T, Message, Theme, Renderer> From<Option<T>>
|
impl<'a, T, Message, Theme, Renderer> From<Option<T>>
|
||||||
for Element<'a, Message, Theme, Renderer>
|
for Element<'a, Message, Theme, Renderer>
|
||||||
where
|
where
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
//! Handle events of a user interface.
|
//! Handle events of a user interface.
|
||||||
use crate::input_method;
|
use crate::input_method;
|
||||||
|
use dnd::DndEvent;
|
||||||
|
use dnd::DndSurface;
|
||||||
|
|
||||||
use crate::keyboard;
|
use crate::keyboard;
|
||||||
use crate::mouse;
|
use crate::mouse;
|
||||||
use crate::touch;
|
use crate::touch;
|
||||||
use crate::window;
|
use crate::window;
|
||||||
|
#[cfg(feature = "wayland")]
|
||||||
|
/// A platform specific event for wayland
|
||||||
|
pub mod wayland;
|
||||||
/// A user interface event.
|
/// A user interface event.
|
||||||
///
|
///
|
||||||
/// _**Note:** This type is largely incomplete! If you need to track
|
/// _**Note:** This type is largely incomplete! If you need to track
|
||||||
|
|
@ -27,6 +32,26 @@ pub enum Event {
|
||||||
|
|
||||||
/// An input method event
|
/// An input method event
|
||||||
InputMethod(input_method::Event),
|
InputMethod(input_method::Event),
|
||||||
|
#[cfg(feature = "a11y")]
|
||||||
|
/// An Accesskit event for a specific Accesskit Node in an accessible widget
|
||||||
|
A11y(
|
||||||
|
crate::widget::Id,
|
||||||
|
iced_accessibility::accesskit::ActionRequest,
|
||||||
|
),
|
||||||
|
|
||||||
|
/// A DnD event.
|
||||||
|
Dnd(DndEvent<DndSurface>),
|
||||||
|
|
||||||
|
/// Platform specific events
|
||||||
|
PlatformSpecific(PlatformSpecific),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A platform specific event
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum PlatformSpecific {
|
||||||
|
#[cfg(feature = "wayland")]
|
||||||
|
/// A Wayland specific event
|
||||||
|
Wayland(wayland::Event),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The status of an [`Event`] after being processed.
|
/// The status of an [`Event`] after being processed.
|
||||||
|
|
|
||||||
10
core/src/event/wayland/layer.rs
Normal file
10
core/src/event/wayland/layer.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
/// layer surface events
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum LayerEvent {
|
||||||
|
/// layer surface Done
|
||||||
|
Done,
|
||||||
|
/// layer surface focused
|
||||||
|
Focused,
|
||||||
|
/// layer_surface unfocused
|
||||||
|
Unfocused,
|
||||||
|
}
|
||||||
39
core/src/event/wayland/mod.rs
Normal file
39
core/src/event/wayland/mod.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
mod layer;
|
||||||
|
mod output;
|
||||||
|
mod popup;
|
||||||
|
mod seat;
|
||||||
|
mod session_lock;
|
||||||
|
mod window;
|
||||||
|
|
||||||
|
use crate::{time::Instant, window::Id};
|
||||||
|
use sctk::reexports::client::protocol::{
|
||||||
|
wl_output::WlOutput, wl_seat::WlSeat, wl_surface::WlSurface,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use layer::*;
|
||||||
|
pub use output::*;
|
||||||
|
pub use popup::*;
|
||||||
|
pub use seat::*;
|
||||||
|
pub use session_lock::*;
|
||||||
|
pub use window::*;
|
||||||
|
|
||||||
|
/// wayland events
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Event {
|
||||||
|
/// layer surface event
|
||||||
|
Layer(LayerEvent, WlSurface, Id),
|
||||||
|
/// popup event
|
||||||
|
Popup(PopupEvent, WlSurface, Id),
|
||||||
|
/// output event
|
||||||
|
Output(OutputEvent, WlOutput),
|
||||||
|
/// window event
|
||||||
|
Window(WindowEvent),
|
||||||
|
/// Seat Event
|
||||||
|
Seat(SeatEvent, WlSeat),
|
||||||
|
/// Session lock events
|
||||||
|
SessionLock(SessionLockEvent),
|
||||||
|
/// Frame events
|
||||||
|
Frame(Instant, WlSurface, Id),
|
||||||
|
/// Request Resize
|
||||||
|
RequestResize,
|
||||||
|
}
|
||||||
34
core/src/event/wayland/output.rs
Normal file
34
core/src/event/wayland/output.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
use sctk::output::OutputInfo;
|
||||||
|
|
||||||
|
/// output events
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum OutputEvent {
|
||||||
|
/// created output
|
||||||
|
Created(Option<OutputInfo>),
|
||||||
|
/// removed output
|
||||||
|
Removed,
|
||||||
|
/// Output Info
|
||||||
|
InfoUpdate(OutputInfo),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for OutputEvent {}
|
||||||
|
|
||||||
|
impl PartialEq for OutputEvent {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::Created(l0), Self::Created(r0)) => {
|
||||||
|
if let Some((l0, r0)) = l0.as_ref().zip(r0.as_ref()) {
|
||||||
|
l0.id == r0.id && l0.make == r0.make && l0.model == r0.model
|
||||||
|
} else {
|
||||||
|
l0.is_none() && r0.is_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Self::InfoUpdate(l0), Self::InfoUpdate(r0)) => {
|
||||||
|
l0.id == r0.id && l0.make == r0.make && l0.model == r0.model
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
core::mem::discriminant(self) == core::mem::discriminant(other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
core/src/event/wayland/popup.rs
Normal file
21
core/src/event/wayland/popup.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
/// popup events
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum PopupEvent {
|
||||||
|
/// Done
|
||||||
|
Done,
|
||||||
|
/// repositioned,
|
||||||
|
Configured {
|
||||||
|
/// x position
|
||||||
|
x: i32,
|
||||||
|
/// y position
|
||||||
|
y: i32,
|
||||||
|
/// width
|
||||||
|
width: u32,
|
||||||
|
/// height
|
||||||
|
height: u32,
|
||||||
|
},
|
||||||
|
/// popup focused
|
||||||
|
Focused,
|
||||||
|
/// popup unfocused
|
||||||
|
Unfocused,
|
||||||
|
}
|
||||||
9
core/src/event/wayland/seat.rs
Normal file
9
core/src/event/wayland/seat.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/// seat events
|
||||||
|
/// Only one seat can interact with an iced_sctk application at a time, but many may interact with the application over the lifetime of the application
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum SeatEvent {
|
||||||
|
/// A new seat is interacting with the application
|
||||||
|
Enter,
|
||||||
|
/// A seat is not interacting with the application anymore
|
||||||
|
Leave,
|
||||||
|
}
|
||||||
19
core/src/event/wayland/session_lock.rs
Normal file
19
core/src/event/wayland/session_lock.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
use crate::window::Id;
|
||||||
|
use sctk::reexports::client::protocol::wl_surface::WlSurface;
|
||||||
|
|
||||||
|
/// session lock events
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum SessionLockEvent {
|
||||||
|
/// Compositor has activated lock
|
||||||
|
Locked,
|
||||||
|
/// Lock rejected / canceled by compositor
|
||||||
|
Finished,
|
||||||
|
/// Session lock protocol not supported
|
||||||
|
NotSupported,
|
||||||
|
/// Session lock surface focused
|
||||||
|
Focused(WlSurface, Id),
|
||||||
|
/// Session lock surface unfocused
|
||||||
|
Unfocused(WlSurface, Id),
|
||||||
|
/// Session unlock has been processed by server
|
||||||
|
Unlocked,
|
||||||
|
}
|
||||||
8
core/src/event/wayland/window.rs
Normal file
8
core/src/event/wayland/window.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
|
/// window events
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub enum WindowEvent {
|
||||||
|
/// Window suggested bounds.
|
||||||
|
SuggestedBounds(Option<crate::Size>),
|
||||||
|
}
|
||||||
170
core/src/id.rs
Normal file
170
core/src/id.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
//! Widget and Window IDs.
|
||||||
|
|
||||||
|
use std::borrow;
|
||||||
|
use std::num::NonZeroU128;
|
||||||
|
use std::sync::atomic::{self, AtomicU64};
|
||||||
|
|
||||||
|
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
|
||||||
|
static NEXT_WINDOW_ID: AtomicU64 = AtomicU64::new(1);
|
||||||
|
|
||||||
|
/// The identifier of a generic widget.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Id(pub Internal);
|
||||||
|
|
||||||
|
impl Id {
|
||||||
|
/// Creates a custom [`Id`].
|
||||||
|
pub fn new(id: impl Into<borrow::Cow<'static, str>>) -> Self {
|
||||||
|
Self(Internal::Custom(Self::next(), id.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// resets the id counter
|
||||||
|
pub fn reset() {
|
||||||
|
NEXT_ID.store(1, atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next() -> u64 {
|
||||||
|
NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a unique [`Id`].
|
||||||
|
///
|
||||||
|
/// This function produces a different [`Id`] every time it is called.
|
||||||
|
pub fn unique() -> Self {
|
||||||
|
let id = Self::next();
|
||||||
|
|
||||||
|
Self(Internal::Unique(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for Id {
|
||||||
|
fn from(value: &'static str) -> Self {
|
||||||
|
Id(Internal::Custom(Id::next(), borrow::Cow::Borrowed(value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<String> for Id {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Id(Internal::Custom(
|
||||||
|
Id::next(),
|
||||||
|
borrow::Cow::Owned(value.to_string()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not meant to be used directly
|
||||||
|
impl From<u64> for Id {
|
||||||
|
fn from(value: u64) -> Self {
|
||||||
|
Self(Internal::Unique(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not meant to be used directly
|
||||||
|
impl From<Id> for NonZeroU128 {
|
||||||
|
fn from(id: Id) -> NonZeroU128 {
|
||||||
|
match &id.0 {
|
||||||
|
Internal::Unique(id) => NonZeroU128::try_from(*id as u128).unwrap(),
|
||||||
|
Internal::Custom(id, _) => {
|
||||||
|
NonZeroU128::try_from(*id as u128).unwrap()
|
||||||
|
}
|
||||||
|
// this is a set id, which is not a valid id and will not ever be converted to a NonZeroU128
|
||||||
|
// so we panic
|
||||||
|
Internal::Set(_) => {
|
||||||
|
panic!("Cannot convert a set id to a NonZeroU128")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for Id {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match &self.0 {
|
||||||
|
Internal::Unique(_) => "Undefined".to_string(),
|
||||||
|
Internal::Custom(_, id) => id.to_string(),
|
||||||
|
Internal::Set(_) => "Set".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX WIndow IDs are made unique by adding u64::MAX to them
|
||||||
|
/// get window node id that won't conflict with other node ids for the duration of the program
|
||||||
|
pub fn window_node_id() -> NonZeroU128 {
|
||||||
|
std::num::NonZeroU128::try_from(
|
||||||
|
u64::MAX as u128
|
||||||
|
+ NEXT_WINDOW_ID.fetch_add(1, atomic::Ordering::Relaxed) as u128,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO refactor to make panic impossible?
|
||||||
|
#[derive(Debug, Clone, Eq)]
|
||||||
|
/// Internal representation of an [`Id`].
|
||||||
|
pub enum Internal {
|
||||||
|
/// a unique id
|
||||||
|
Unique(u64),
|
||||||
|
/// a custom id, which is equal to any [`Id`] with a matching number or string
|
||||||
|
Custom(u64, borrow::Cow<'static, str>),
|
||||||
|
/// XXX Do not use this as an id for an accessibility node, it will panic!
|
||||||
|
/// XXX Only meant to be used for widgets that have multiple accessibility nodes, each with a
|
||||||
|
/// unique or custom id
|
||||||
|
/// an Id Set, which is equal to any [`Id`] with a matching number or string
|
||||||
|
Set(Vec<Self>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Internal {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::Unique(l0), Self::Unique(r0)) => l0 == r0,
|
||||||
|
(Self::Custom(_, l1), Self::Custom(_, r1)) => l1 == r1,
|
||||||
|
(Self::Set(l0), Self::Set(r0)) => l0 == r0,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Similar to PartialEq, but only intended for use when comparing Ids
|
||||||
|
pub trait IdEq {
|
||||||
|
/// Compare two Ids for equality based on their number or name
|
||||||
|
fn eq(&self, other: &Self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdEq for Internal {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::Unique(l0), Self::Unique(r0)) => l0 == r0,
|
||||||
|
(Self::Custom(l0, l1), Self::Custom(r0, r1)) => {
|
||||||
|
l0 == r0 || l1 == r1
|
||||||
|
}
|
||||||
|
// allow custom ids to be equal to unique ids
|
||||||
|
(Self::Unique(l0), Self::Custom(r0, _))
|
||||||
|
| (Self::Custom(l0, _), Self::Unique(r0)) => l0 == r0,
|
||||||
|
(Self::Set(l0), Self::Set(r0)) => l0 == r0,
|
||||||
|
// allow set ids to just be equal to any of their members
|
||||||
|
(Self::Set(l0), r) | (r, Self::Set(l0)) => {
|
||||||
|
l0.iter().any(|l| l == r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::hash::Hash for Internal {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
match self {
|
||||||
|
Self::Unique(id) => id.hash(state),
|
||||||
|
Self::Custom(name, _) => name.hash(state),
|
||||||
|
Self::Set(ids) => ids.hash(state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Id;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unique_generates_different_ids() {
|
||||||
|
let a = Id::unique();
|
||||||
|
let b = Id::unique();
|
||||||
|
|
||||||
|
assert_ne!(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,7 @@ impl Image<Handle> {
|
||||||
border_radius: border::Radius::default(),
|
border_radius: border::Radius::default(),
|
||||||
opacity: 1.0,
|
opacity: 1.0,
|
||||||
snap: false,
|
snap: false,
|
||||||
|
// border_radius: [0.0; 4],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,7 +84,7 @@ impl From<&Handle> for Image {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A handle of some image data.
|
/// A handle of some image data.
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum Handle {
|
pub enum Handle {
|
||||||
/// A file handle. The image data will be read
|
/// A file handle. The image data will be read
|
||||||
/// from the file path.
|
/// from the file path.
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::SmolStr;
|
||||||
///
|
///
|
||||||
/// [`winit`]: https://docs.rs/winit/0.30/winit/keyboard/enum.Key.html
|
/// [`winit`]: https://docs.rs/winit/0.30/winit/keyboard/enum.Key.html
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub enum Key<C = SmolStr> {
|
pub enum Key<C = SmolStr> {
|
||||||
/// A key with an established name.
|
/// A key with an established name.
|
||||||
Named(Named),
|
Named(Named),
|
||||||
|
|
@ -129,6 +130,7 @@ impl From<Named> for Key {
|
||||||
///
|
///
|
||||||
/// [`winit`]: https://docs.rs/winit/0.30/winit/keyboard/enum.Key.html
|
/// [`winit`]: https://docs.rs/winit/0.30/winit/keyboard/enum.Key.html
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
pub enum Named {
|
pub enum Named {
|
||||||
/// The `Alt` (Alternative) key.
|
/// The `Alt` (Alternative) key.
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ mod background;
|
||||||
mod color;
|
mod color;
|
||||||
mod content_fit;
|
mod content_fit;
|
||||||
mod element;
|
mod element;
|
||||||
|
#[cfg(not(feature = "a11y"))]
|
||||||
|
pub mod id;
|
||||||
mod length;
|
mod length;
|
||||||
mod pixels;
|
mod pixels;
|
||||||
mod point;
|
mod point;
|
||||||
|
|
@ -61,6 +63,9 @@ pub use element::Element;
|
||||||
pub use event::Event;
|
pub use event::Event;
|
||||||
pub use font::Font;
|
pub use font::Font;
|
||||||
pub use gradient::Gradient;
|
pub use gradient::Gradient;
|
||||||
|
|
||||||
|
#[cfg(feature = "a11y")]
|
||||||
|
pub use iced_accessibility::id;
|
||||||
pub use image::Image;
|
pub use image::Image;
|
||||||
pub use input_method::InputMethod;
|
pub use input_method::InputMethod;
|
||||||
pub use layout::Layout;
|
pub use layout::Layout;
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@ impl Click {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
self.position.distance(new_position) < 6.0
|
self.position.distance(new_position) < 6.0
|
||||||
&& duration
|
&& duration
|
||||||
.map(|duration| duration.as_millis() <= 300)
|
.map(|duration| duration.as_millis() <= 300)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ pub use nested::Nested;
|
||||||
use crate::layout;
|
use crate::layout;
|
||||||
use crate::mouse;
|
use crate::mouse;
|
||||||
use crate::renderer;
|
use crate::renderer;
|
||||||
use crate::widget;
|
use crate::widget::Operation;
|
||||||
use crate::widget::Tree;
|
use crate::widget::Tree;
|
||||||
use crate::{Clipboard, Event, Layout, Rectangle, Shell, Size, Vector};
|
use crate::{Clipboard, Event, Layout, Rectangle, Shell, Size, Vector};
|
||||||
|
|
||||||
|
|
@ -37,12 +37,12 @@ where
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Applies a [`widget::Operation`] to the [`Overlay`].
|
/// Applies an [`Operation`] to the [`Overlay`].
|
||||||
fn operate(
|
fn operate(
|
||||||
&mut self,
|
&mut self,
|
||||||
_layout: Layout<'_>,
|
_layout: Layout<'_>,
|
||||||
_renderer: &Renderer,
|
_renderer: &Renderer,
|
||||||
_operation: &mut dyn widget::Operation,
|
_operation: &mut dyn crate::widget::Operation,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ use crate::mouse;
|
||||||
use crate::overlay;
|
use crate::overlay;
|
||||||
use crate::renderer;
|
use crate::renderer;
|
||||||
use crate::widget;
|
use crate::widget;
|
||||||
use crate::{Clipboard, Event, Layout, Overlay, Shell, Size};
|
use crate::widget::Operation;
|
||||||
|
use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size};
|
||||||
|
|
||||||
/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`]
|
/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`]
|
||||||
/// children.
|
/// children.
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,17 @@ impl From<[u16; 2]> for Padding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<[u16; 4]> for Padding {
|
||||||
|
fn from(p: [u16; 4]) -> Self {
|
||||||
|
Padding {
|
||||||
|
top: f32::from(p[0]),
|
||||||
|
right: f32::from(p[1]),
|
||||||
|
bottom: f32::from(p[2]),
|
||||||
|
left: f32::from(p[3]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<f32> for Padding {
|
impl From<f32> for Padding {
|
||||||
fn from(p: f32) -> Self {
|
fn from(p: f32) -> Self {
|
||||||
Padding {
|
Padding {
|
||||||
|
|
@ -235,6 +246,18 @@ impl From<[f32; 2]> for Padding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<[f32; 4]> for Padding {
|
||||||
|
/// [top, rght, bottom, left]
|
||||||
|
fn from(p: [f32; 4]) -> Self {
|
||||||
|
Padding {
|
||||||
|
top: p[0],
|
||||||
|
right: p[1],
|
||||||
|
bottom: p[2],
|
||||||
|
left: p[3],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Padding> for Size {
|
impl From<Padding> for Size {
|
||||||
fn from(padding: Padding) -> Self {
|
fn from(padding: Padding) -> Self {
|
||||||
Self::new(padding.x(), padding.y())
|
Self::new(padding.x(), padding.y())
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,18 @@ impl Rectangle<f32> {
|
||||||
|
|
||||||
/// Returns true if the current [`Rectangle`] is within the given
|
/// Returns true if the current [`Rectangle`] is within the given
|
||||||
/// `container`. Includes the right and bottom edges.
|
/// `container`. Includes the right and bottom edges.
|
||||||
|
/// Returns true if the given [`Point`] is contained in the [`Rectangle`].
|
||||||
|
/// The [`Point`] must be strictly contained, i.e. it must not be on the
|
||||||
|
/// border.
|
||||||
|
pub fn contains_strict(&self, point: Point) -> bool {
|
||||||
|
self.x < point.x
|
||||||
|
&& point.x < self.x + self.width
|
||||||
|
&& self.y < point.y
|
||||||
|
&& point.y < self.y + self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the current [`Rectangle`] is completely within the given
|
||||||
|
/// `container`.
|
||||||
pub fn is_within(&self, container: &Rectangle) -> bool {
|
pub fn is_within(&self, container: &Rectangle) -> bool {
|
||||||
self.x >= container.x
|
self.x >= container.x
|
||||||
&& self.y >= container.y
|
&& self.y >= container.y
|
||||||
|
|
@ -195,6 +207,16 @@ impl Rectangle<f32> {
|
||||||
&& self.y + self.height <= container.y + container.height
|
&& self.y + self.height <= container.y + container.height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the current [`Rectangle`] is completely within the given
|
||||||
|
/// `container`. The [`Rectangle`] must be strictly contained, i.e. it must
|
||||||
|
/// not be on the border.
|
||||||
|
pub fn is_within_strict(&self, container: &Rectangle) -> bool {
|
||||||
|
container.contains_strict(self.position())
|
||||||
|
&& container.contains_strict(
|
||||||
|
self.position() + Vector::new(self.width, self.height),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Computes the intersection with the given [`Rectangle`].
|
/// Computes the intersection with the given [`Rectangle`].
|
||||||
pub fn intersection(
|
pub fn intersection(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
||||||
|
|
@ -104,14 +104,20 @@ impl Default for Quad {
|
||||||
/// The styling attributes of a [`Renderer`].
|
/// The styling attributes of a [`Renderer`].
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Style {
|
pub struct Style {
|
||||||
|
/// The color to apply to symbolic icons.
|
||||||
|
pub icon_color: Color,
|
||||||
/// The text color
|
/// The text color
|
||||||
pub text_color: Color,
|
pub text_color: Color,
|
||||||
|
/// The scale factor
|
||||||
|
pub scale_factor: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Style {
|
impl Default for Style {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Style {
|
Style {
|
||||||
|
icon_color: Color::BLACK,
|
||||||
text_color: Color::BLACK,
|
text_color: Color::BLACK,
|
||||||
|
scale_factor: 1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ impl text::Renderer for () {
|
||||||
type Font = Font;
|
type Font = Font;
|
||||||
type Paragraph = ();
|
type Paragraph = ();
|
||||||
type Editor = ();
|
type Editor = ();
|
||||||
|
type Raw = ();
|
||||||
|
|
||||||
const ICON_FONT: Font = Font::DEFAULT;
|
const ICON_FONT: Font = Font::DEFAULT;
|
||||||
const CHECKMARK_ICON: char = '0';
|
const CHECKMARK_ICON: char = '0';
|
||||||
|
|
@ -56,7 +57,7 @@ impl text::Renderer for () {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_size(&self) -> Pixels {
|
fn default_size(&self) -> Pixels {
|
||||||
Pixels(16.0)
|
Pixels(14.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_paragraph(
|
fn fill_paragraph(
|
||||||
|
|
@ -77,6 +78,8 @@ impl text::Renderer for () {
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fill_raw(&mut self, _raw: Self::Raw) {}
|
||||||
|
|
||||||
fn fill_text(
|
fn fill_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
_paragraph: Text,
|
_paragraph: Text,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@ pub struct Settings {
|
||||||
///
|
///
|
||||||
/// By default, it is enabled.
|
/// By default, it is enabled.
|
||||||
pub vsync: bool,
|
pub vsync: bool,
|
||||||
|
|
||||||
|
/// If set to true the application will exit when the main window is closed.
|
||||||
|
pub exit_on_close_request: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
|
|
@ -48,9 +51,20 @@ impl Default for Settings {
|
||||||
id: None,
|
id: None,
|
||||||
fonts: Vec::new(),
|
fonts: Vec::new(),
|
||||||
default_font: Font::default(),
|
default_font: Font::default(),
|
||||||
default_text_size: Pixels(16.0),
|
vsync: false,
|
||||||
antialiasing: true,
|
default_text_size: Pixels(14.0),
|
||||||
vsync: true,
|
antialiasing: false,
|
||||||
|
exit_on_close_request: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "winit")]
|
||||||
|
impl From<Settings> for iced_winit::Settings {
|
||||||
|
fn from(settings: Settings) -> iced_winit::Settings {
|
||||||
|
iced_winit::Settings {
|
||||||
|
id: settings.id,
|
||||||
|
fonts: settings.fonts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ pub struct Svg<H = Handle> {
|
||||||
///
|
///
|
||||||
/// 0 means transparent. 1 means opaque.
|
/// 0 means transparent. 1 means opaque.
|
||||||
pub opacity: f32,
|
pub opacity: f32,
|
||||||
|
|
||||||
|
/// The border radius for the svg
|
||||||
|
pub border_radius: [f32; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Svg<Handle> {
|
impl Svg<Handle> {
|
||||||
|
|
@ -39,6 +42,7 @@ impl Svg<Handle> {
|
||||||
color: None,
|
color: None,
|
||||||
rotation: Radians(0.0),
|
rotation: Radians(0.0),
|
||||||
opacity: 1.0,
|
opacity: 1.0,
|
||||||
|
border_radius: [0.0; 4],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,6 +63,12 @@ impl Svg<Handle> {
|
||||||
self.opacity = opacity.into();
|
self.opacity = opacity.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the border radius of the [`Svg`]
|
||||||
|
pub fn border_radius(mut self, border_radius: impl Into<[f32; 4]>) -> Self {
|
||||||
|
self.border_radius = border_radius.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Handle> for Svg {
|
impl From<&Handle> for Svg {
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ impl LineHeight {
|
||||||
|
|
||||||
impl Default for LineHeight {
|
impl Default for LineHeight {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::Relative(1.3)
|
Self::Relative(1.4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -299,6 +299,9 @@ pub trait Renderer: crate::Renderer {
|
||||||
/// The [`Editor`] of this [`Renderer`].
|
/// The [`Editor`] of this [`Renderer`].
|
||||||
type Editor: Editor<Font = Self::Font> + 'static;
|
type Editor: Editor<Font = Self::Font> + 'static;
|
||||||
|
|
||||||
|
/// The Raw of this [`Renderer`].
|
||||||
|
type Raw;
|
||||||
|
|
||||||
/// The icon font of the backend.
|
/// The icon font of the backend.
|
||||||
const ICON_FONT: Self::Font;
|
const ICON_FONT: Self::Font;
|
||||||
|
|
||||||
|
|
@ -363,6 +366,9 @@ pub trait Renderer: crate::Renderer {
|
||||||
clip_bounds: Rectangle,
|
clip_bounds: Rectangle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Draws the given Raw
|
||||||
|
fn fill_raw(&mut self, raw: Self::Raw);
|
||||||
|
|
||||||
/// Draws the given [`Text`] at the given position and with the given
|
/// Draws the given [`Text`] at the given position and with the given
|
||||||
/// [`Color`].
|
/// [`Color`].
|
||||||
fn fill_text(
|
fn fill_text(
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,9 @@ pub struct Style {
|
||||||
|
|
||||||
/// The default text [`Color`] of the application.
|
/// The default text [`Color`] of the application.
|
||||||
pub text_color: Color,
|
pub text_color: Color,
|
||||||
|
|
||||||
|
/// The default icon [`iced_core::Color`] of the application.
|
||||||
|
pub icon_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The default blank style of a theme.
|
/// The default blank style of a theme.
|
||||||
|
|
@ -332,5 +335,6 @@ pub fn default(theme: &Theme) -> Style {
|
||||||
Style {
|
Style {
|
||||||
background_color: palette.background.base.color,
|
background_color: palette.background.base.color,
|
||||||
text_color: palette.background.base.text,
|
text_color: palette.background.base.text,
|
||||||
|
icon_color: palette.background.base.text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -421,12 +421,15 @@ impl Extended {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A pair of background and text colors.
|
/// Recommended background, icon, and text [`Color`].
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Pair {
|
pub struct Pair {
|
||||||
/// The background color.
|
/// The background color.
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
|
|
||||||
|
/// The icon color, which defaults to the text color.
|
||||||
|
pub icon: Color,
|
||||||
|
|
||||||
/// The text color.
|
/// The text color.
|
||||||
///
|
///
|
||||||
/// It's guaranteed to be readable on top of the background [`color`].
|
/// It's guaranteed to be readable on top of the background [`color`].
|
||||||
|
|
@ -438,9 +441,12 @@ pub struct Pair {
|
||||||
impl Pair {
|
impl Pair {
|
||||||
/// Creates a new [`Pair`] from a background [`Color`] and some text [`Color`].
|
/// Creates a new [`Pair`] from a background [`Color`] and some text [`Color`].
|
||||||
pub fn new(color: Color, text: Color) -> Self {
|
pub fn new(color: Color, text: Color) -> Self {
|
||||||
|
let text = readable(color, text);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
color,
|
color,
|
||||||
text: readable(color, text),
|
icon: text,
|
||||||
|
text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ pub mod operation;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
pub mod tree;
|
pub mod tree;
|
||||||
|
|
||||||
mod id;
|
pub use crate::id::Id;
|
||||||
|
|
||||||
pub use id::Id;
|
|
||||||
pub use operation::Operation;
|
pub use operation::Operation;
|
||||||
pub use text::Text;
|
pub use text::Text;
|
||||||
pub use tree::Tree;
|
pub use tree::Tree;
|
||||||
|
|
@ -92,7 +90,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reconciles the [`Widget`] with the provided [`Tree`].
|
/// Reconciles the [`Widget`] with the provided [`Tree`].
|
||||||
fn diff(&self, tree: &mut Tree) {
|
fn diff(&mut self, tree: &mut Tree) {
|
||||||
tree.children.clear();
|
tree.children.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,4 +145,35 @@ where
|
||||||
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
|
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "a11y")]
|
||||||
|
/// get the a11y nodes for the widget and its children
|
||||||
|
fn a11y_nodes(
|
||||||
|
&self,
|
||||||
|
_layout: Layout<'_>,
|
||||||
|
_state: &Tree,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
) -> iced_accessibility::A11yTree {
|
||||||
|
iced_accessibility::A11yTree::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the id of the widget
|
||||||
|
fn id(&self) -> Option<Id> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the id of the widget
|
||||||
|
/// This may be called while diffing the widget tree
|
||||||
|
fn set_id(&mut self, _id: Id) {}
|
||||||
|
|
||||||
|
/// Adds the drag destination rectangles of the widget.
|
||||||
|
/// Runs after the layout phase for each widget in the tree.
|
||||||
|
fn drag_destinations(
|
||||||
|
&self,
|
||||||
|
_state: &Tree,
|
||||||
|
_layout: Layout<'_>,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles,
|
||||||
|
) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
//! Query or update internal widget state.
|
//! Query or update internal widget state.
|
||||||
pub mod focusable;
|
pub mod focusable;
|
||||||
pub mod scrollable;
|
pub mod scrollable;
|
||||||
|
pub mod search_id;
|
||||||
pub mod text_input;
|
pub mod text_input;
|
||||||
|
|
||||||
pub use focusable::Focusable;
|
pub use focusable::Focusable;
|
||||||
|
|
@ -525,7 +526,7 @@ where
|
||||||
|
|
||||||
/// Produces an [`Operation`] that applies the given [`Operation`] to the
|
/// Produces an [`Operation`] that applies the given [`Operation`] to the
|
||||||
/// children of a container with the given [`Id`].
|
/// children of a container with the given [`Id`].
|
||||||
pub fn scope<T: 'static>(
|
pub fn scoped<T: 'static>(
|
||||||
target: Id,
|
target: Id,
|
||||||
operation: impl Operation<T> + 'static,
|
operation: impl Operation<T> + 'static,
|
||||||
) -> impl Operation<T> {
|
) -> impl Operation<T> {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
//! Operate on widgets that can be focused.
|
//! Operate on widgets that can be focused.
|
||||||
use crate::Rectangle;
|
use crate::Rectangle;
|
||||||
|
use crate::id::IdEq;
|
||||||
use crate::widget::Id;
|
use crate::widget::Id;
|
||||||
use crate::widget::operation::{self, Operation, Outcome};
|
use crate::widget::operation::{self, Operation, Outcome};
|
||||||
|
|
||||||
|
|
@ -39,7 +40,7 @@ pub fn focus<T>(target: Id) -> impl Operation<T> {
|
||||||
state: &mut dyn Focusable,
|
state: &mut dyn Focusable,
|
||||||
) {
|
) {
|
||||||
match id {
|
match id {
|
||||||
Some(id) if id == &self.target => {
|
Some(id) if IdEq::eq(&id.0, &self.target.0) => {
|
||||||
state.focus();
|
state.focus();
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
|
||||||
49
core/src/widget/operation/search_id.rs
Normal file
49
core/src/widget/operation/search_id.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
//! Search for widgets with the target Id.
|
||||||
|
|
||||||
|
use super::Operation;
|
||||||
|
use crate::{
|
||||||
|
Rectangle,
|
||||||
|
id::Id,
|
||||||
|
widget::operation::{Outcome, focusable::Count},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Produces an [`Operation`] that searches for the Id
|
||||||
|
pub fn search_id(target: Id) -> impl Operation<Id> {
|
||||||
|
struct Find {
|
||||||
|
found: bool,
|
||||||
|
target: Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operation<Id> for Find {
|
||||||
|
fn custom(
|
||||||
|
&mut self,
|
||||||
|
id: Option<&Id>,
|
||||||
|
_bounds: Rectangle,
|
||||||
|
_state: &mut dyn std::any::Any,
|
||||||
|
) {
|
||||||
|
if Some(&self.target) == id {
|
||||||
|
self.found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&self) -> Outcome<Id> {
|
||||||
|
if self.found {
|
||||||
|
Outcome::Some(self.target.clone())
|
||||||
|
} else {
|
||||||
|
Outcome::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn traverse(
|
||||||
|
&mut self,
|
||||||
|
operate: &mut dyn FnMut(&mut dyn Operation<Id>),
|
||||||
|
) {
|
||||||
|
operate(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Find {
|
||||||
|
found: false,
|
||||||
|
target,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,13 +24,14 @@ use crate::alignment;
|
||||||
use crate::layout;
|
use crate::layout;
|
||||||
use crate::mouse;
|
use crate::mouse;
|
||||||
use crate::renderer;
|
use crate::renderer;
|
||||||
use crate::text;
|
|
||||||
use crate::text::paragraph::{self, Paragraph};
|
use crate::text::paragraph::{self, Paragraph};
|
||||||
|
use crate::text::{self, Fragment};
|
||||||
use crate::widget::tree::{self, Tree};
|
use crate::widget::tree::{self, Tree};
|
||||||
use crate::{
|
use crate::{
|
||||||
Color, Element, Layout, Length, Pixels, Rectangle, Size, Theme, Widget,
|
Color, Element, Layout, Length, Pixels, Rectangle, Size, Theme, Widget,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
pub use text::{Alignment, LineHeight, Shaping, Wrapping};
|
pub use text::{Alignment, LineHeight, Shaping, Wrapping};
|
||||||
|
|
||||||
/// A bunch of text.
|
/// A bunch of text.
|
||||||
|
|
@ -60,6 +61,7 @@ where
|
||||||
Theme: Catalog,
|
Theme: Catalog,
|
||||||
Renderer: text::Renderer,
|
Renderer: text::Renderer,
|
||||||
{
|
{
|
||||||
|
id: crate::widget::Id,
|
||||||
fragment: text::Fragment<'a>,
|
fragment: text::Fragment<'a>,
|
||||||
format: Format<Renderer::Font>,
|
format: Format<Renderer::Font>,
|
||||||
class: Theme::Class<'a>,
|
class: Theme::Class<'a>,
|
||||||
|
|
@ -73,6 +75,7 @@ where
|
||||||
/// Create a new fragment of [`Text`] with the given contents.
|
/// Create a new fragment of [`Text`] with the given contents.
|
||||||
pub fn new(fragment: impl text::IntoFragment<'a>) -> Self {
|
pub fn new(fragment: impl text::IntoFragment<'a>) -> Self {
|
||||||
Text {
|
Text {
|
||||||
|
id: crate::widget::Id::unique(),
|
||||||
fragment: fragment.into_fragment(),
|
fragment: fragment.into_fragment(),
|
||||||
format: Format::default(),
|
format: Format::default(),
|
||||||
class: Theme::default(),
|
class: Theme::default(),
|
||||||
|
|
@ -263,6 +266,50 @@ where
|
||||||
) {
|
) {
|
||||||
operation.text(None, layout.bounds(), &self.fragment);
|
operation.text(None, layout.bounds(), &self.fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "a11y")]
|
||||||
|
fn a11y_nodes(
|
||||||
|
&self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_state: &Tree,
|
||||||
|
_: mouse::Cursor,
|
||||||
|
) -> iced_accessibility::A11yTree {
|
||||||
|
use iced_accessibility::{
|
||||||
|
A11yTree,
|
||||||
|
accesskit::{Live, Node, Rect, Role},
|
||||||
|
};
|
||||||
|
|
||||||
|
let Rectangle {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
} = layout.bounds();
|
||||||
|
let bounds = Rect::new(
|
||||||
|
x as f64,
|
||||||
|
y as f64,
|
||||||
|
(x + width) as f64,
|
||||||
|
(y + height) as f64,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut node = Node::new(Role::Paragraph);
|
||||||
|
|
||||||
|
// TODO is the name likely different from the content?
|
||||||
|
node.set_label(self.fragment.to_string().into_boxed_str());
|
||||||
|
node.set_bounds(bounds);
|
||||||
|
|
||||||
|
// TODO make this configurable
|
||||||
|
node.set_live(Live::Polite);
|
||||||
|
A11yTree::leaf(node, self.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<crate::widget::Id> {
|
||||||
|
Some(self.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_id(&mut self, id: crate::widget::Id) {
|
||||||
|
self.id = id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The format of some [`Text`].
|
/// The format of some [`Text`].
|
||||||
|
|
@ -370,6 +417,29 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// impl<'a, Theme, Renderer> Clone for Text<'a, Theme, Renderer>
|
||||||
|
// where
|
||||||
|
// Renderer: text::Renderer,
|
||||||
|
// {
|
||||||
|
// fn clone(&self) -> Self {
|
||||||
|
// Self {
|
||||||
|
// id: self.id.clone(),
|
||||||
|
// content: self.content.clone(),
|
||||||
|
// size: self.size,
|
||||||
|
// line_height: self.line_height,
|
||||||
|
// width: self.width,
|
||||||
|
// height: self.height,
|
||||||
|
// horizontal_alignment: self.horizontal_alignment,
|
||||||
|
// vertical_alignment: self.vertical_alignment,
|
||||||
|
// font: self.font,
|
||||||
|
// style: self.style,
|
||||||
|
// shaping: self.shaping,
|
||||||
|
// wrap: self.wrap,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// TODO(POP): Clone no longer can be implemented because of style being a Box(style)
|
||||||
|
|
||||||
impl<'a, Theme, Renderer> From<&'a str> for Text<'a, Theme, Renderer>
|
impl<'a, Theme, Renderer> From<&'a str> for Text<'a, Theme, Renderer>
|
||||||
where
|
where
|
||||||
Theme: Catalog + 'a,
|
Theme: Catalog + 'a,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
//! Store internal widget state in a state tree to ensure continuity.
|
//! Store internal widget state in a state tree to ensure continuity.
|
||||||
use crate::Widget;
|
use crate::Widget;
|
||||||
|
use crate::id::{Id, Internal};
|
||||||
use std::any::{self, Any};
|
use std::any::{self, Any};
|
||||||
use std::borrow::Borrow;
|
use std::borrow::{Borrow, BorrowMut, Cow};
|
||||||
use std::fmt;
|
use std::collections::HashMap;
|
||||||
|
use std::hash::Hash;
|
||||||
|
use std::{fmt, mem};
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
/// A map of named widget states.
|
||||||
|
pub static NAMED: std::cell::RefCell<HashMap<Cow<'static, str>, (State, Vec<(usize, Tree)>)>> = std::cell::RefCell::new(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
/// A persistent state widget tree.
|
/// A persistent state widget tree.
|
||||||
///
|
///
|
||||||
|
|
@ -13,6 +20,9 @@ pub struct Tree {
|
||||||
/// The tag of the [`Tree`].
|
/// The tag of the [`Tree`].
|
||||||
pub tag: Tag,
|
pub tag: Tag,
|
||||||
|
|
||||||
|
/// the Id of the [`Tree`]
|
||||||
|
pub id: Option<Id>,
|
||||||
|
|
||||||
/// The [`State`] of the [`Tree`].
|
/// The [`State`] of the [`Tree`].
|
||||||
pub state: State,
|
pub state: State,
|
||||||
|
|
||||||
|
|
@ -24,6 +34,7 @@ impl Tree {
|
||||||
/// Creates an empty, stateless [`Tree`] with no children.
|
/// Creates an empty, stateless [`Tree`] with no children.
|
||||||
pub fn empty() -> Self {
|
pub fn empty() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
id: None,
|
||||||
tag: Tag::stateless(),
|
tag: Tag::stateless(),
|
||||||
state: State::None,
|
state: State::None,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
|
|
@ -40,13 +51,104 @@ impl Tree {
|
||||||
let widget = widget.borrow();
|
let widget = widget.borrow();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
id: widget.id(),
|
||||||
tag: widget.tag(),
|
tag: widget.tag(),
|
||||||
state: widget.state(),
|
state: widget.state(),
|
||||||
children: widget.children(),
|
children: widget.children(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reconciles the current tree with the provided [`Widget`].
|
/// Takes all named widgets from the tree.
|
||||||
|
pub fn take_all_named(
|
||||||
|
&mut self,
|
||||||
|
) -> HashMap<Cow<'static, str>, (State, Vec<(usize, Tree)>)> {
|
||||||
|
let mut named = HashMap::new();
|
||||||
|
struct Visit {
|
||||||
|
parent: Cow<'static, str>,
|
||||||
|
index: usize,
|
||||||
|
visited: bool,
|
||||||
|
}
|
||||||
|
// tree traversal to find all named widgets
|
||||||
|
// and keep their state and children
|
||||||
|
let mut stack = vec![(self, None)];
|
||||||
|
while let Some((tree, visit)) = stack.pop() {
|
||||||
|
if let Some(Id(Internal::Custom(_, n))) = tree.id.clone() {
|
||||||
|
let state = mem::replace(&mut tree.state, State::None);
|
||||||
|
let children_count = tree.children.len();
|
||||||
|
let children =
|
||||||
|
tree.children.iter_mut().enumerate().rev().map(|(i, c)| {
|
||||||
|
if matches!(c.id, Some(Id(Internal::Custom(_, _)))) {
|
||||||
|
(c, None)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
c,
|
||||||
|
Some(Visit {
|
||||||
|
index: i,
|
||||||
|
parent: n.clone(),
|
||||||
|
visited: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_ = named.insert(
|
||||||
|
n.clone(),
|
||||||
|
(state, Vec::with_capacity(children_count)),
|
||||||
|
);
|
||||||
|
stack.extend(children);
|
||||||
|
} else if let Some(visit) = visit {
|
||||||
|
if visit.visited {
|
||||||
|
named.get_mut(&visit.parent).unwrap().1.push((
|
||||||
|
visit.index,
|
||||||
|
mem::replace(
|
||||||
|
tree,
|
||||||
|
Tree {
|
||||||
|
id: tree.id.clone(),
|
||||||
|
tag: tree.tag,
|
||||||
|
..Tree::empty()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
let ptr = tree as *mut Tree;
|
||||||
|
|
||||||
|
stack.push((
|
||||||
|
// TODO remove this unsafe block
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
// SAFETY: when the reference is finally accessed, all the children references will have been processed first.
|
||||||
|
unsafe {
|
||||||
|
ptr.as_mut().unwrap()
|
||||||
|
},
|
||||||
|
Some(Visit {
|
||||||
|
visited: true,
|
||||||
|
..visit
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
stack.extend(tree.children.iter_mut().map(|c| (c, None)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stack.extend(tree.children.iter_mut().map(|s| (s, None)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
named
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds a widget state in the tree by its id.
|
||||||
|
pub fn find<'a>(&'a self, id: &Id) -> Option<&'a Tree> {
|
||||||
|
if self.id == Some(id.clone()) {
|
||||||
|
return Some(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in self.children.iter() {
|
||||||
|
if let Some(tree) = child.find(id) {
|
||||||
|
return Some(tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconciliates the current tree with the provided [`Widget`].
|
||||||
///
|
///
|
||||||
/// If the tag of the [`Widget`] matches the tag of the [`Tree`], then the
|
/// If the tag of the [`Widget`] matches the tag of the [`Tree`], then the
|
||||||
/// [`Widget`] proceeds with the reconciliation (i.e. [`Widget::diff`] is called).
|
/// [`Widget`] proceeds with the reconciliation (i.e. [`Widget::diff`] is called).
|
||||||
|
|
@ -56,53 +158,203 @@ impl Tree {
|
||||||
/// [`Widget::diff`]: crate::Widget::diff
|
/// [`Widget::diff`]: crate::Widget::diff
|
||||||
pub fn diff<'a, Message, Theme, Renderer>(
|
pub fn diff<'a, Message, Theme, Renderer>(
|
||||||
&mut self,
|
&mut self,
|
||||||
new: impl Borrow<dyn Widget<Message, Theme, Renderer> + 'a>,
|
mut new: impl BorrowMut<dyn Widget<Message, Theme, Renderer> + 'a>,
|
||||||
) where
|
) where
|
||||||
Renderer: crate::Renderer,
|
Renderer: crate::Renderer,
|
||||||
{
|
{
|
||||||
if self.tag == new.borrow().tag() {
|
let borrowed: &mut dyn Widget<Message, Theme, Renderer> =
|
||||||
new.borrow().diff(self);
|
new.borrow_mut();
|
||||||
|
let mut needs_reset = false;
|
||||||
|
let tag_match = self.tag == borrowed.tag();
|
||||||
|
if let Some(Id(Internal::Custom(_, n))) = borrowed.id() {
|
||||||
|
if let Some((mut state, children)) = NAMED
|
||||||
|
.with(|named| named.borrow_mut().remove(&n))
|
||||||
|
.or_else(|| {
|
||||||
|
//check self.id
|
||||||
|
if let Some(Id(Internal::Custom(_, ref name))) = self.id {
|
||||||
|
if name == &n {
|
||||||
|
Some((
|
||||||
|
mem::replace(&mut self.state, State::None),
|
||||||
|
self.children
|
||||||
|
.iter_mut()
|
||||||
|
.map(|s| {
|
||||||
|
// take the data
|
||||||
|
mem::replace(
|
||||||
|
s,
|
||||||
|
Tree {
|
||||||
|
id: s.id.clone(),
|
||||||
|
tag: s.tag,
|
||||||
|
..Tree::empty()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.enumerate()
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
*self = Self::new(new);
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
std::mem::swap(&mut self.state, &mut state);
|
||||||
|
let widget_children = borrowed.children();
|
||||||
|
if !tag_match || self.children.len() != widget_children.len() {
|
||||||
|
self.children = borrowed.children();
|
||||||
|
} else {
|
||||||
|
for (old_i, mut old) in children {
|
||||||
|
let Some(my_state) = self.children.get_mut(old_i)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if my_state.tag != old.tag || {
|
||||||
|
!match (&old.id, &my_state.id) {
|
||||||
|
(
|
||||||
|
Some(Id(Internal::Custom(_, old_name))),
|
||||||
|
Some(Id(Internal::Custom(_, my_name))),
|
||||||
|
) => old_name == my_name,
|
||||||
|
(
|
||||||
|
Some(Id(Internal::Set(a))),
|
||||||
|
Some(Id(Internal::Set(b))),
|
||||||
|
) => a.len() == b.len(),
|
||||||
|
(
|
||||||
|
Some(Id(Internal::Unique(_))),
|
||||||
|
Some(Id(Internal::Unique(_))),
|
||||||
|
) => true,
|
||||||
|
(None, None) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
mem::swap(my_state, &mut old);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
needs_reset = true;
|
||||||
|
}
|
||||||
|
} else if tag_match {
|
||||||
|
if let Some(id) = self.id.clone() {
|
||||||
|
borrowed.set_id(id);
|
||||||
|
}
|
||||||
|
if self.children.len() != borrowed.children().len() {
|
||||||
|
self.children = borrowed.children();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
needs_reset = true;
|
||||||
|
}
|
||||||
|
if needs_reset {
|
||||||
|
*self = Self::new(borrowed);
|
||||||
|
let borrowed = new.borrow_mut();
|
||||||
|
borrowed.diff(self);
|
||||||
|
} else {
|
||||||
|
borrowed.diff(self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reconciles the children of the tree with the provided list of widgets.
|
/// Reconciles the children of the tree with the provided list of widgets.
|
||||||
pub fn diff_children<'a, Message, Theme, Renderer>(
|
pub fn diff_children<'a, Message, Theme, Renderer>(
|
||||||
&mut self,
|
&mut self,
|
||||||
new_children: &[impl Borrow<dyn Widget<Message, Theme, Renderer> + 'a>],
|
new_children: &mut [impl BorrowMut<
|
||||||
|
dyn Widget<Message, Theme, Renderer> + 'a,
|
||||||
|
>],
|
||||||
) where
|
) where
|
||||||
Renderer: crate::Renderer,
|
Renderer: crate::Renderer,
|
||||||
{
|
{
|
||||||
self.diff_children_custom(
|
self.diff_children_custom(
|
||||||
new_children,
|
new_children,
|
||||||
|tree, widget| tree.diff(widget.borrow()),
|
new_children.iter().map(|c| c.borrow().id()).collect(),
|
||||||
|widget| Self::new(widget.borrow()),
|
|tree, widget| {
|
||||||
);
|
let borrowed: &mut dyn Widget<_, _, _> = widget.borrow_mut();
|
||||||
|
tree.diff(borrowed)
|
||||||
|
},
|
||||||
|
|widget| {
|
||||||
|
let borrowed: &dyn Widget<_, _, _> = widget.borrow();
|
||||||
|
Self::new(borrowed)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reconciles the children of the tree with the provided list of widgets using custom
|
/// Reconciles the children of the tree with the provided list of widgets using custom
|
||||||
/// logic both for diffing and creating new widget state.
|
/// logic both for diffing and creating new widget state.
|
||||||
pub fn diff_children_custom<T>(
|
pub fn diff_children_custom<T>(
|
||||||
&mut self,
|
&mut self,
|
||||||
new_children: &[T],
|
new_children: &mut [T],
|
||||||
diff: impl Fn(&mut Tree, &T),
|
new_ids: Vec<Option<Id>>,
|
||||||
|
diff: impl Fn(&mut Tree, &mut T),
|
||||||
new_state: impl Fn(&T) -> Self,
|
new_state: impl Fn(&T) -> Self,
|
||||||
) {
|
) {
|
||||||
if self.children.len() > new_children.len() {
|
if self.children.len() > new_children.len() {
|
||||||
self.children.truncate(new_children.len());
|
self.children.truncate(new_children.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (child_state, new) in
|
let len_changed = self.children.len() != new_children.len();
|
||||||
self.children.iter_mut().zip(new_children.iter())
|
|
||||||
|
let children_len = self.children.len();
|
||||||
|
let (mut id_map, mut id_list): (
|
||||||
|
HashMap<String, &mut Tree>,
|
||||||
|
Vec<&mut Tree>,
|
||||||
|
) = self.children.iter_mut().fold(
|
||||||
|
(HashMap::new(), Vec::with_capacity(children_len)),
|
||||||
|
|(mut id_map, mut id_list), c| {
|
||||||
|
if let Some(id) = c.id.as_ref() {
|
||||||
|
if let Internal::Custom(_, ref name) = id.0 {
|
||||||
|
let _ = id_map.insert(name.to_string(), c);
|
||||||
|
} else {
|
||||||
|
id_list.push(c);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id_list.push(c);
|
||||||
|
}
|
||||||
|
(id_map, id_list)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut child_state_i = 0;
|
||||||
|
let mut new_trees: Vec<(Tree, usize)> =
|
||||||
|
Vec::with_capacity(new_children.len());
|
||||||
|
for (i, (new, new_id)) in
|
||||||
|
new_children.iter_mut().zip(new_ids.iter()).enumerate()
|
||||||
{
|
{
|
||||||
|
let child_state = if let Some(c) = new_id.as_ref().and_then(|id| {
|
||||||
|
if let Internal::Custom(_, ref name) = id.0 {
|
||||||
|
id_map.remove(name.as_ref())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
c
|
||||||
|
} else if child_state_i < id_list.len()
|
||||||
|
&& !matches!(
|
||||||
|
id_list[child_state_i].id,
|
||||||
|
Some(Id(Internal::Custom(_, _)))
|
||||||
|
)
|
||||||
|
{
|
||||||
|
let c = &mut id_list[child_state_i];
|
||||||
|
if len_changed {
|
||||||
|
c.id.clone_from(new_id);
|
||||||
|
}
|
||||||
|
child_state_i += 1;
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
let mut my_new_state = new_state(new);
|
||||||
|
diff(&mut my_new_state, new);
|
||||||
|
new_trees.push((my_new_state, i));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
diff(child_state, new);
|
diff(child_state, new);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.children.len() < new_children.len() {
|
for (new_tree, i) in new_trees {
|
||||||
self.children.extend(
|
if self.children.len() > i {
|
||||||
new_children[self.children.len()..].iter().map(new_state),
|
self.children[i] = new_tree;
|
||||||
);
|
} else {
|
||||||
|
self.children.push(new_tree);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,8 +366,8 @@ impl Tree {
|
||||||
/// `maybe_changed` closure.
|
/// `maybe_changed` closure.
|
||||||
pub fn diff_children_custom_with_search<T>(
|
pub fn diff_children_custom_with_search<T>(
|
||||||
current_children: &mut Vec<Tree>,
|
current_children: &mut Vec<Tree>,
|
||||||
new_children: &[T],
|
new_children: &mut [T],
|
||||||
diff: impl Fn(&mut Tree, &T),
|
diff: impl Fn(&mut Tree, &mut T),
|
||||||
maybe_changed: impl Fn(usize) -> bool,
|
maybe_changed: impl Fn(usize) -> bool,
|
||||||
new_state: impl Fn(&T) -> Tree,
|
new_state: impl Fn(&T) -> Tree,
|
||||||
) {
|
) {
|
||||||
|
|
@ -183,7 +435,7 @@ pub fn diff_children_custom_with_search<T>(
|
||||||
|
|
||||||
// TODO: Merge loop with extend logic (?)
|
// TODO: Merge loop with extend logic (?)
|
||||||
for (child_state, new) in
|
for (child_state, new) in
|
||||||
current_children.iter_mut().zip(new_children.iter())
|
current_children.iter_mut().zip(new_children.iter_mut())
|
||||||
{
|
{
|
||||||
diff(child_state, new);
|
diff(child_state, new);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ pub enum Event {
|
||||||
/// ## Platform-specific
|
/// ## Platform-specific
|
||||||
///
|
///
|
||||||
/// - **Wayland:** Not implemented.
|
/// - **Wayland:** Not implemented.
|
||||||
FileHovered(PathBuf),
|
FileHovered(Vec<PathBuf>),
|
||||||
|
|
||||||
/// A file has been dropped into the window.
|
/// A file has been dropped into the window.
|
||||||
///
|
///
|
||||||
|
|
@ -63,7 +63,7 @@ pub enum Event {
|
||||||
/// ## Platform-specific
|
/// ## Platform-specific
|
||||||
///
|
///
|
||||||
/// - **Wayland:** Not implemented.
|
/// - **Wayland:** Not implemented.
|
||||||
FileDropped(PathBuf),
|
FileDropped(Vec<PathBuf>),
|
||||||
|
|
||||||
/// A file was hovered, but has exited the window.
|
/// A file was hovered, but has exited the window.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,20 @@ pub struct Id(u64);
|
||||||
static COUNT: AtomicU64 = AtomicU64::new(1);
|
static COUNT: AtomicU64 = AtomicU64::new(1);
|
||||||
|
|
||||||
impl Id {
|
impl Id {
|
||||||
|
/// No window will match this Id
|
||||||
|
pub const NONE: Id = Id(0);
|
||||||
|
pub const RESERVED: Id = Id(1);
|
||||||
|
|
||||||
/// Creates a new unique window [`Id`].
|
/// Creates a new unique window [`Id`].
|
||||||
pub fn unique() -> Id {
|
pub fn unique() -> Id {
|
||||||
|
let id = Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed));
|
||||||
|
if id.0 == 0 {
|
||||||
|
Id(COUNT.fetch_add(2, atomic::Ordering::Relaxed))
|
||||||
|
} else if id.0 == 1 {
|
||||||
Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed))
|
Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed))
|
||||||
|
} else {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use std::fmt::{Debug, Formatter};
|
||||||
|
|
||||||
/// Data of a screenshot, captured with `window::screenshot()`.
|
/// Data of a screenshot, captured with `window::screenshot()`.
|
||||||
///
|
///
|
||||||
/// The `bytes` of this screenshot will always be ordered as `RGBA` in the `sRGB` color space.
|
/// The `bytes` of this screenshot will always be ordered as `RGBA` in the sRGB color space.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Screenshot {
|
pub struct Screenshot {
|
||||||
/// The RGBA bytes of the [`Screenshot`].
|
/// The RGBA bytes of the [`Screenshot`].
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,8 @@ pub struct Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
fn default() -> Self {
|
fn default() -> Settings {
|
||||||
Self {
|
Settings {
|
||||||
size: Size::new(1024.0, 768.0),
|
size: Size::new(1024.0, 768.0),
|
||||||
maximized: false,
|
maximized: false,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ where
|
||||||
state.style(&self.program, theme)
|
state.style(&self.program, theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scale_factor(&self, state: &Self::State, window: window::Id) -> f32 {
|
fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 {
|
||||||
state.scale_factor(&self.program, window)
|
state.scale_factor(&self.program, window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -405,7 +405,7 @@ where
|
||||||
program.style(self.state(), theme)
|
program.style(self.state(), theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scale_factor(&self, program: &P, window: window::Id) -> f32 {
|
pub fn scale_factor(&self, program: &P, window: window::Id) -> f64 {
|
||||||
program.scale_factor(self.state(), window)
|
program.scale_factor(self.state(), window)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
name = "counter"
|
name = "counter"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
@ -10,7 +10,4 @@ iced.workspace = true
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
iced.features = ["webgl", "fira-sans"]
|
iced.features = ["webgl"]
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
iced_test.workspace = true
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
iced.features = ["highlighter", "tokio", "debug"]
|
iced.features = ["highlighter", "tokio", "debug", "winit", "tiny-skia"]
|
||||||
|
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tokio.features = ["fs"]
|
tokio.features = ["fs"]
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
iced.features = ["debug", "canvas", "tokio"]
|
iced.features = ["debug", "canvas", "tokio", "winit", "tiny-skia"]
|
||||||
|
|
||||||
itertools = "0.12"
|
itertools = "0.12"
|
||||||
rustc-hash.workspace = true
|
rustc-hash.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
iced.features = ["debug"]
|
iced.features = ["debug", "winit", "wgpu"]
|
||||||
|
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
|
|
|
||||||
|
|
@ -27,4 +27,4 @@ iced_wgpu.features = ["webgl"]
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
console_log = "1.0"
|
console_log = "1.0"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
web-sys = { version = "0.3", features = ["Element", "HtmlCanvasElement", "Window", "Document"] }
|
web-sys = { version = "=0.3", features = ["Element", "HtmlCanvasElement", "Window", "Document"] }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use iced_wgpu::core::window::Id;
|
||||||
use iced_wgpu::Renderer;
|
use iced_wgpu::Renderer;
|
||||||
use iced_widget::{bottom, column, row, slider, text, text_input};
|
use iced_widget::{bottom, column, row, slider, text, text_input};
|
||||||
use iced_winit::core::{Color, Element, Theme};
|
use iced_winit::core::{Color, Element, Theme};
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ mod scene;
|
||||||
use controls::Controls;
|
use controls::Controls;
|
||||||
use scene::Scene;
|
use scene::Scene;
|
||||||
|
|
||||||
|
use iced_wgpu::core::window::Id;
|
||||||
|
use iced_wgpu::graphics::Viewport;
|
||||||
use iced_wgpu::graphics::{Shell, Viewport};
|
use iced_wgpu::graphics::{Shell, Viewport};
|
||||||
use iced_wgpu::{Engine, Renderer, wgpu};
|
use iced_wgpu::{Engine, Renderer, wgpu};
|
||||||
use iced_winit::Clipboard;
|
use iced_winit::Clipboard;
|
||||||
|
|
@ -54,7 +56,10 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl winit::application::ApplicationHandler for Runner {
|
impl winit::application::ApplicationHandler for Runner {
|
||||||
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
|
fn resumed(
|
||||||
|
&mut self,
|
||||||
|
event_loop: &dyn winit::event_loop::ActiveEventLoop,
|
||||||
|
) {
|
||||||
if let Self::Loading = self {
|
if let Self::Loading = self {
|
||||||
let window = Arc::new(
|
let window = Arc::new(
|
||||||
event_loop
|
event_loop
|
||||||
|
|
@ -183,7 +188,7 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
|
||||||
|
|
||||||
fn window_event(
|
fn window_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event_loop: &winit::event_loop::ActiveEventLoop,
|
event_loop: &dyn winit::event_loop::ActiveEventLoop,
|
||||||
_window_id: winit::window::WindowId,
|
_window_id: winit::window::WindowId,
|
||||||
event: WindowEvent,
|
event: WindowEvent,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,4 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
iced.features = ["debug", "lazy"]
|
iced.features = ["debug", "lazy", "async-std", "winit", "tiny-skia"]
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,6 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
iced.features = ["advanced", "canvas"]
|
iced.features = ["advanced", "canvas", "winit"]
|
||||||
|
|
||||||
lyon_algorithms = "1.0"
|
lyon_algorithms = "1.0"
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ mod loupe {
|
||||||
self.content.as_widget().children()
|
self.content.as_widget().children()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff(&self, tree: &mut widget::Tree) {
|
fn diff(&mut self, tree: &mut widget::Tree) {
|
||||||
self.content.as_widget().diff(tree);
|
self.content.as_widget().diff(tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,11 @@ edition = "2024"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced = { path = "../..", features = ["debug"] }
|
iced = { path = "../..", default-features = false, features = [
|
||||||
|
"a11y",
|
||||||
|
"tokio",
|
||||||
|
"debug",
|
||||||
|
"winit",
|
||||||
|
"multi-window",
|
||||||
|
"tiny-skia",
|
||||||
|
] }
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ struct Window {
|
||||||
scale_input: String,
|
scale_input: String,
|
||||||
current_scale: f32,
|
current_scale: f32,
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
|
input_id: text_input::Id,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
iced.features = ["image", "debug", "tokio"]
|
iced.features = ["image", "debug", "tokio", "winit", "tiny-skia"]
|
||||||
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
use iced::widget::scrollable::{self, Properties, Scrollbar, Scroller};
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
button, column, container, operation, progress_bar, radio, row, scrollable,
|
button, column, container, operation, progress_bar, radio, row, scrollable,
|
||||||
slider, space, text,
|
slider, space, text,
|
||||||
};
|
};
|
||||||
use iced::{Border, Center, Color, Element, Fill, Task, Theme};
|
use iced::{Border, Center, Color, Element, Fill, Task, Theme};
|
||||||
|
use iced_core::id::Id;
|
||||||
|
|
||||||
pub fn main() -> iced::Result {
|
pub fn main() -> iced::Result {
|
||||||
iced::application(
|
iced::application(
|
||||||
|
|
|
||||||
19
examples/sctk_drag/Cargo.toml
Normal file
19
examples/sctk_drag/Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "sctk_drag"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iced_core = { path = "../../core" }
|
||||||
|
iced = { path = "../..", default-features = false, features = [
|
||||||
|
"tiny-skia",
|
||||||
|
"tokio",
|
||||||
|
"winit",
|
||||||
|
"wayland",
|
||||||
|
"debug",
|
||||||
|
] }
|
||||||
|
env_logger = "0.10"
|
||||||
|
# sctk = { package = "smithay-client-toolkit", path = "../../../fork/client-toolkit/" }
|
||||||
|
sctk.workspace = true
|
||||||
728
examples/sctk_drag/src/dnd_destination.rs
Normal file
728
examples/sctk_drag/src/dnd_destination.rs
Normal file
|
|
@ -0,0 +1,728 @@
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
sync::atomic::{AtomicU64, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
|
use iced::{
|
||||||
|
clipboard::{
|
||||||
|
dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent},
|
||||||
|
mime::AllowedMimeTypes,
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
id::Internal,
|
||||||
|
mouse, overlay, Event, Length, Rectangle,
|
||||||
|
};
|
||||||
|
use iced::{id::Id, Element};
|
||||||
|
use iced_core::{
|
||||||
|
self, layout,
|
||||||
|
widget::{tree, Tree},
|
||||||
|
Clipboard, Layout, Shell, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn dnd_destination<'a, Message: 'static>(
|
||||||
|
child: impl Into<Element<'a, Message>>,
|
||||||
|
mimes: Vec<Cow<'static, str>>,
|
||||||
|
) -> DndDestination<'a, Message> {
|
||||||
|
DndDestination::new(child, mimes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dnd_destination_for_data<T: AllowedMimeTypes, Message: 'static>(
|
||||||
|
child: impl Into<Element<'static, Message>>,
|
||||||
|
on_finish: impl Fn(Option<T>, DndAction) -> Message + 'static,
|
||||||
|
) -> DndDestination<'static, Message> {
|
||||||
|
DndDestination::for_data(child, on_finish)
|
||||||
|
}
|
||||||
|
|
||||||
|
static DRAG_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct DragId(pub u128);
|
||||||
|
|
||||||
|
impl DragId {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
DragId(
|
||||||
|
u128::from(u64::MAX)
|
||||||
|
+ u128::from(DRAG_ID_COUNTER.fetch_add(1, Ordering::Relaxed)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
|
impl Default for DragId {
|
||||||
|
fn default() -> Self {
|
||||||
|
DragId::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DndDestination<'a, Message> {
|
||||||
|
id: Id,
|
||||||
|
drag_id: Option<u64>,
|
||||||
|
preferred_action: DndAction,
|
||||||
|
action: DndAction,
|
||||||
|
container: Element<'a, Message>,
|
||||||
|
mime_types: Vec<Cow<'static, str>>,
|
||||||
|
forward_drag_as_cursor: bool,
|
||||||
|
on_hold: Option<Box<dyn Fn(f64, f64) -> Message>>,
|
||||||
|
on_drop: Option<Box<dyn Fn(f64, f64) -> Message>>,
|
||||||
|
on_enter: Option<Box<dyn Fn(f64, f64, Vec<String>) -> Message>>,
|
||||||
|
on_leave: Option<Box<dyn Fn() -> Message>>,
|
||||||
|
on_motion: Option<Box<dyn Fn(f64, f64) -> Message>>,
|
||||||
|
on_action_selected: Option<Box<dyn Fn(DndAction) -> Message>>,
|
||||||
|
on_data_received: Option<Box<dyn Fn(String, Vec<u8>) -> Message>>,
|
||||||
|
on_finish:
|
||||||
|
Option<Box<dyn Fn(String, Vec<u8>, DndAction, f64, f64) -> Message>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message: 'static> DndDestination<'a, Message> {
|
||||||
|
pub fn new(
|
||||||
|
child: impl Into<Element<'a, Message>>,
|
||||||
|
mimes: Vec<Cow<'static, str>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Id::unique(),
|
||||||
|
drag_id: None,
|
||||||
|
mime_types: mimes,
|
||||||
|
preferred_action: DndAction::Move,
|
||||||
|
action: DndAction::Copy | DndAction::Move,
|
||||||
|
container: child.into(),
|
||||||
|
forward_drag_as_cursor: false,
|
||||||
|
on_hold: None,
|
||||||
|
on_drop: None,
|
||||||
|
on_enter: None,
|
||||||
|
on_leave: None,
|
||||||
|
on_motion: None,
|
||||||
|
on_action_selected: None,
|
||||||
|
on_data_received: None,
|
||||||
|
on_finish: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_data<T: AllowedMimeTypes>(
|
||||||
|
child: impl Into<Element<'a, Message>>,
|
||||||
|
on_finish: impl Fn(Option<T>, DndAction) -> Message + 'static,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Id::unique(),
|
||||||
|
drag_id: None,
|
||||||
|
mime_types: T::allowed().iter().cloned().map(Cow::Owned).collect(),
|
||||||
|
preferred_action: DndAction::Move,
|
||||||
|
action: DndAction::Copy | DndAction::Move,
|
||||||
|
container: child.into(),
|
||||||
|
forward_drag_as_cursor: false,
|
||||||
|
on_hold: None,
|
||||||
|
on_drop: None,
|
||||||
|
on_enter: None,
|
||||||
|
on_leave: None,
|
||||||
|
on_motion: None,
|
||||||
|
on_action_selected: None,
|
||||||
|
on_data_received: None,
|
||||||
|
on_finish: Some(Box::new(move |mime, data, action, _, _| {
|
||||||
|
on_finish(T::try_from((data, mime)).ok(), action)
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn data_received_for<T: AllowedMimeTypes>(
|
||||||
|
mut self,
|
||||||
|
f: impl Fn(Option<T>) -> Message + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_data_received =
|
||||||
|
Some(Box::new(
|
||||||
|
move |mime, data| f(T::try_from((data, mime)).ok()),
|
||||||
|
));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_id(
|
||||||
|
child: impl Into<Element<'a, Message>>,
|
||||||
|
id: Id,
|
||||||
|
mimes: Vec<Cow<'static, str>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
drag_id: None,
|
||||||
|
mime_types: mimes,
|
||||||
|
preferred_action: DndAction::Move,
|
||||||
|
action: DndAction::Copy | DndAction::Move,
|
||||||
|
container: child.into(),
|
||||||
|
forward_drag_as_cursor: false,
|
||||||
|
on_hold: None,
|
||||||
|
on_drop: None,
|
||||||
|
on_enter: None,
|
||||||
|
on_leave: None,
|
||||||
|
on_motion: None,
|
||||||
|
on_action_selected: None,
|
||||||
|
on_data_received: None,
|
||||||
|
on_finish: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn drag_id(mut self, id: u64) -> Self {
|
||||||
|
self.drag_id = Some(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn action(mut self, action: DndAction) -> Self {
|
||||||
|
self.action = action;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn preferred_action(mut self, action: DndAction) -> Self {
|
||||||
|
self.preferred_action = action;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn forward_drag_as_cursor(mut self, forward: bool) -> Self {
|
||||||
|
self.forward_drag_as_cursor = forward;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_hold(
|
||||||
|
mut self,
|
||||||
|
f: impl Fn(f64, f64) -> Message + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_hold = Some(Box::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_drop(
|
||||||
|
mut self,
|
||||||
|
f: impl Fn(f64, f64) -> Message + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_drop = Some(Box::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_enter(
|
||||||
|
mut self,
|
||||||
|
f: impl Fn(f64, f64, Vec<String>) -> Message + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_enter = Some(Box::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_leave(mut self, m: impl Fn() -> Message + 'static) -> Self {
|
||||||
|
self.on_leave = Some(Box::new(m));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_finish(
|
||||||
|
mut self,
|
||||||
|
f: impl Fn(String, Vec<u8>, DndAction, f64, f64) -> Message + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_finish = Some(Box::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_motion(
|
||||||
|
mut self,
|
||||||
|
f: impl Fn(f64, f64) -> Message + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_motion = Some(Box::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_action_selected(
|
||||||
|
mut self,
|
||||||
|
f: impl Fn(DndAction) -> Message + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_action_selected = Some(Box::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_data_received(
|
||||||
|
mut self,
|
||||||
|
f: impl Fn(String, Vec<u8>) -> Message + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_data_received = Some(Box::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the drag id of the destination.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if the destination has been assigned a Set id, which is invalid.
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_drag_id(&self) -> u128 {
|
||||||
|
u128::from(self.drag_id.unwrap_or_else(|| match &self.id.0 {
|
||||||
|
Internal::Unique(id) | Internal::Custom(id, _) => *id,
|
||||||
|
Internal::Set(_) => {
|
||||||
|
panic!("Invalid Id assigned to dnd destination.")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message: 'static> Widget<Message, iced::Theme, iced::Renderer>
|
||||||
|
for DndDestination<'a, Message>
|
||||||
|
{
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
vec![Tree::new(&self.container)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag(&self) -> iced_core::widget::tree::Tag {
|
||||||
|
tree::Tag::of::<State<()>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&mut self, tree: &mut Tree) {
|
||||||
|
tree.children[0].diff(self.container.as_widget_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> iced_core::widget::tree::State {
|
||||||
|
tree::State::new(State::<()>::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> iced_core::Size<Length> {
|
||||||
|
self.container.as_widget().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
self.container.as_widget().layout(
|
||||||
|
&mut tree.children[0],
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: layout::Layout<'_>,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
operation: &mut dyn iced_core::widget::Operation<()>,
|
||||||
|
) {
|
||||||
|
self.container.as_widget().operate(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
fn on_event(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: Event,
|
||||||
|
layout: layout::Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) -> event::Status {
|
||||||
|
let s = self.container.as_widget_mut().on_event(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event.clone(),
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
if matches!(s, event::Status::Captured) {
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = tree.state.downcast_mut::<State<()>>();
|
||||||
|
|
||||||
|
let my_id = self.get_drag_id();
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Dnd(DndEvent::Offer(
|
||||||
|
id,
|
||||||
|
OfferEvent::Enter {
|
||||||
|
x, y, mime_types, ..
|
||||||
|
},
|
||||||
|
)) if id == Some(my_id) => {
|
||||||
|
if let Some(msg) = state.on_enter(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
mime_types,
|
||||||
|
self.on_enter.as_ref().map(std::convert::AsRef::as_ref),
|
||||||
|
(),
|
||||||
|
) {
|
||||||
|
shell.publish(msg);
|
||||||
|
}
|
||||||
|
if self.forward_drag_as_cursor {
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
let drag_cursor =
|
||||||
|
mouse::Cursor::Available((x as f32, y as f32).into());
|
||||||
|
let event = Event::Mouse(mouse::Event::CursorMoved {
|
||||||
|
position: drag_cursor.position().unwrap(),
|
||||||
|
});
|
||||||
|
self.container.as_widget_mut().on_event(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
layout,
|
||||||
|
drag_cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
Event::Dnd(DndEvent::Offer(id, OfferEvent::Leave))
|
||||||
|
if id == Some(my_id) =>
|
||||||
|
{
|
||||||
|
state.on_leave(
|
||||||
|
self.on_leave.as_ref().map(std::convert::AsRef::as_ref),
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.forward_drag_as_cursor {
|
||||||
|
let drag_cursor = mouse::Cursor::Unavailable;
|
||||||
|
let event = Event::Mouse(mouse::Event::CursorLeft);
|
||||||
|
self.container.as_widget_mut().on_event(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
layout,
|
||||||
|
drag_cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y }))
|
||||||
|
if id == Some(my_id) =>
|
||||||
|
{
|
||||||
|
if let Some(msg) = state.on_motion(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
self.on_motion.as_ref().map(std::convert::AsRef::as_ref),
|
||||||
|
self.on_enter.as_ref().map(std::convert::AsRef::as_ref),
|
||||||
|
(),
|
||||||
|
) {
|
||||||
|
shell.publish(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.forward_drag_as_cursor {
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
let drag_cursor =
|
||||||
|
mouse::Cursor::Available((x as f32, y as f32).into());
|
||||||
|
let event = Event::Mouse(mouse::Event::CursorMoved {
|
||||||
|
position: drag_cursor.position().unwrap(),
|
||||||
|
});
|
||||||
|
self.container.as_widget_mut().on_event(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
layout,
|
||||||
|
drag_cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination))
|
||||||
|
if id == Some(my_id) =>
|
||||||
|
{
|
||||||
|
if let Some(msg) = state.on_leave(
|
||||||
|
self.on_leave.as_ref().map(std::convert::AsRef::as_ref),
|
||||||
|
) {
|
||||||
|
shell.publish(msg);
|
||||||
|
}
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop))
|
||||||
|
if id == Some(my_id) =>
|
||||||
|
{
|
||||||
|
if let Some(msg) = state.on_drop(
|
||||||
|
self.on_drop.as_ref().map(std::convert::AsRef::as_ref),
|
||||||
|
) {
|
||||||
|
shell.publish(msg);
|
||||||
|
}
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
Event::Dnd(DndEvent::Offer(
|
||||||
|
id,
|
||||||
|
OfferEvent::SelectedAction(action),
|
||||||
|
)) if id == Some(my_id) => {
|
||||||
|
if let Some(msg) = state.on_action_selected(
|
||||||
|
action,
|
||||||
|
self.on_action_selected
|
||||||
|
.as_ref()
|
||||||
|
.map(std::convert::AsRef::as_ref),
|
||||||
|
) {
|
||||||
|
shell.publish(msg);
|
||||||
|
}
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
Event::Dnd(DndEvent::Offer(
|
||||||
|
id,
|
||||||
|
OfferEvent::Data { data, mime_type },
|
||||||
|
)) if id == Some(my_id) => {
|
||||||
|
dbg!("got data");
|
||||||
|
if let (Some(msg), ret) = state.on_data_received(
|
||||||
|
mime_type,
|
||||||
|
data,
|
||||||
|
self.on_data_received
|
||||||
|
.as_ref()
|
||||||
|
.map(std::convert::AsRef::as_ref),
|
||||||
|
self.on_finish.as_ref().map(std::convert::AsRef::as_ref),
|
||||||
|
) {
|
||||||
|
shell.publish(msg);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
event::Status::Ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: layout::Layout<'_>,
|
||||||
|
cursor_position: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.container.as_widget().mouse_interaction(
|
||||||
|
&tree.children[0],
|
||||||
|
layout,
|
||||||
|
cursor_position,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut iced::Renderer,
|
||||||
|
theme: &iced::Theme,
|
||||||
|
renderer_style: &iced_core::renderer::Style,
|
||||||
|
layout: layout::Layout<'_>,
|
||||||
|
cursor_position: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.container.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
renderer_style,
|
||||||
|
layout,
|
||||||
|
cursor_position,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
translation: iced::Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, iced::Theme, iced::Renderer>>
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drag_destinations(
|
||||||
|
&self,
|
||||||
|
state: &Tree,
|
||||||
|
layout: layout::Layout<'_>,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let my_id = self.get_drag_id();
|
||||||
|
let my_dest = DndDestinationRectangle {
|
||||||
|
id: my_id,
|
||||||
|
rectangle: dnd::Rectangle {
|
||||||
|
x: f64::from(bounds.x),
|
||||||
|
y: f64::from(bounds.y),
|
||||||
|
width: f64::from(bounds.width),
|
||||||
|
height: f64::from(bounds.height),
|
||||||
|
},
|
||||||
|
mime_types: self.mime_types.clone(),
|
||||||
|
actions: self.action,
|
||||||
|
preferred: self.preferred_action,
|
||||||
|
};
|
||||||
|
dnd_rectangles.push(my_dest);
|
||||||
|
|
||||||
|
self.container.as_widget().drag_destinations(
|
||||||
|
&state.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
dnd_rectangles,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<Id> {
|
||||||
|
Some(self.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_id(&mut self, id: Id) {
|
||||||
|
self.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct State<T> {
|
||||||
|
pub drag_offer: Option<DragOffer<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DragOffer<T> {
|
||||||
|
pub x: f64,
|
||||||
|
pub y: f64,
|
||||||
|
pub dropped: bool,
|
||||||
|
pub selected_action: DndAction,
|
||||||
|
pub data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> State<T> {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { drag_offer: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_enter<Message>(
|
||||||
|
&mut self,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
mime_types: Vec<String>,
|
||||||
|
on_enter: Option<impl Fn(f64, f64, Vec<String>) -> Message>,
|
||||||
|
data: T,
|
||||||
|
) -> Option<Message> {
|
||||||
|
self.drag_offer = Some(DragOffer {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
dropped: false,
|
||||||
|
selected_action: DndAction::empty(),
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
on_enter.map(|f| f(x, y, mime_types))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_leave<Message>(
|
||||||
|
&mut self,
|
||||||
|
on_leave: Option<&dyn Fn() -> Message>,
|
||||||
|
) -> Option<Message> {
|
||||||
|
if self.drag_offer.as_ref().is_some_and(|d| !d.dropped) {
|
||||||
|
self.drag_offer = None;
|
||||||
|
on_leave.map(|f| f())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_motion<Message>(
|
||||||
|
&mut self,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
on_motion: Option<impl Fn(f64, f64) -> Message>,
|
||||||
|
on_enter: Option<impl Fn(f64, f64, Vec<String>) -> Message>,
|
||||||
|
data: T,
|
||||||
|
) -> Option<Message> {
|
||||||
|
if let Some(s) = self.drag_offer.as_mut() {
|
||||||
|
s.x = x;
|
||||||
|
s.y = y;
|
||||||
|
} else {
|
||||||
|
self.drag_offer = Some(DragOffer {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
dropped: false,
|
||||||
|
selected_action: DndAction::empty(),
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
if let Some(f) = on_enter {
|
||||||
|
return Some(f(x, y, vec![]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
on_motion.map(|f| f(x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_drop<Message>(
|
||||||
|
&mut self,
|
||||||
|
on_drop: Option<impl Fn(f64, f64) -> Message>,
|
||||||
|
) -> Option<Message> {
|
||||||
|
if let Some(offer) = self.drag_offer.as_mut() {
|
||||||
|
offer.dropped = true;
|
||||||
|
if let Some(f) = on_drop {
|
||||||
|
return Some(f(offer.x, offer.y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_action_selected<Message>(
|
||||||
|
&mut self,
|
||||||
|
action: DndAction,
|
||||||
|
on_action_selected: Option<impl Fn(DndAction) -> Message>,
|
||||||
|
) -> Option<Message> {
|
||||||
|
if let Some(s) = self.drag_offer.as_mut() {
|
||||||
|
s.selected_action = action;
|
||||||
|
}
|
||||||
|
if let Some(f) = on_action_selected {
|
||||||
|
f(action).into()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_data_received<Message>(
|
||||||
|
&mut self,
|
||||||
|
mime: String,
|
||||||
|
data: Vec<u8>,
|
||||||
|
on_data_received: Option<impl Fn(String, Vec<u8>) -> Message>,
|
||||||
|
on_finish: Option<
|
||||||
|
impl Fn(String, Vec<u8>, DndAction, f64, f64) -> Message,
|
||||||
|
>,
|
||||||
|
) -> (Option<Message>, event::Status) {
|
||||||
|
dbg!("data received");
|
||||||
|
let Some(dnd) = self.drag_offer.as_ref() else {
|
||||||
|
self.drag_offer = None;
|
||||||
|
return (None, event::Status::Ignored);
|
||||||
|
};
|
||||||
|
|
||||||
|
if dnd.dropped {
|
||||||
|
let ret = (
|
||||||
|
on_finish
|
||||||
|
.map(|f| f(mime, data, dnd.selected_action, dnd.x, dnd.y)),
|
||||||
|
event::Status::Captured,
|
||||||
|
);
|
||||||
|
self.drag_offer = None;
|
||||||
|
ret
|
||||||
|
} else if let Some(f) = on_data_received {
|
||||||
|
(Some(f(mime, data)), event::Status::Captured)
|
||||||
|
} else {
|
||||||
|
(None, event::Status::Ignored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message: 'static> From<DndDestination<'a, Message>>
|
||||||
|
for Element<'a, Message>
|
||||||
|
{
|
||||||
|
fn from(wrapper: DndDestination<'a, Message>) -> Self {
|
||||||
|
Element::new(wrapper)
|
||||||
|
}
|
||||||
|
}
|
||||||
382
examples/sctk_drag/src/dnd_source.rs
Normal file
382
examples/sctk_drag/src/dnd_source.rs
Normal file
|
|
@ -0,0 +1,382 @@
|
||||||
|
use std::any::Any;
|
||||||
|
|
||||||
|
use iced::id::Id;
|
||||||
|
use iced::widget::container;
|
||||||
|
use iced::Element;
|
||||||
|
use iced::{
|
||||||
|
clipboard::dnd::{DndAction, DndEvent, SourceEvent},
|
||||||
|
event, mouse, overlay, Event, Length, Point, Rectangle,
|
||||||
|
};
|
||||||
|
use iced_core::{
|
||||||
|
layout, renderer,
|
||||||
|
widget::{tree, Tree},
|
||||||
|
Clipboard, Shell,
|
||||||
|
};
|
||||||
|
use iced_core::{Layout, Widget};
|
||||||
|
|
||||||
|
pub fn dnd_source<
|
||||||
|
'a,
|
||||||
|
Message: 'static,
|
||||||
|
AppMessage: 'static,
|
||||||
|
D: iced::clipboard::mime::AsMimeTypes + Send + 'static,
|
||||||
|
>(
|
||||||
|
child: impl Into<Element<'a, Message>>,
|
||||||
|
) -> DndSource<'a, Message, AppMessage, D> {
|
||||||
|
DndSource::new(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DndSource<'a, Message, AppMessage, D> {
|
||||||
|
id: Id,
|
||||||
|
action: DndAction,
|
||||||
|
container: Element<'a, Message>,
|
||||||
|
drag_content: Option<Box<dyn Fn() -> D>>,
|
||||||
|
drag_icon:
|
||||||
|
Option<Box<dyn Fn() -> (Element<'static, AppMessage>, tree::State)>>,
|
||||||
|
drag_threshold: f32,
|
||||||
|
_phantom: std::marker::PhantomData<AppMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
'a,
|
||||||
|
Message: 'static,
|
||||||
|
AppMessage: 'static,
|
||||||
|
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
|
||||||
|
> DndSource<'a, Message, AppMessage, D>
|
||||||
|
{
|
||||||
|
pub fn new(child: impl Into<Element<'a, Message>>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Id::unique(),
|
||||||
|
action: DndAction::Copy | DndAction::Move,
|
||||||
|
container: container(child).into(),
|
||||||
|
drag_content: None,
|
||||||
|
drag_icon: None,
|
||||||
|
drag_threshold: 8.0,
|
||||||
|
_phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_id(child: impl Into<Element<'a, Message>>, id: Id) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
action: DndAction::Copy | DndAction::Move,
|
||||||
|
container: container(child).into(),
|
||||||
|
drag_content: None,
|
||||||
|
drag_icon: None,
|
||||||
|
drag_threshold: 8.0,
|
||||||
|
_phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn action(mut self, action: DndAction) -> Self {
|
||||||
|
self.action = action;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn drag_content(mut self, f: impl Fn() -> D + 'static) -> Self {
|
||||||
|
self.drag_content = Some(Box::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn drag_icon(
|
||||||
|
mut self,
|
||||||
|
f: impl Fn() -> (Element<'static, AppMessage>, tree::State) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.drag_icon = Some(Box::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn drag_threshold(mut self, threshold: f32) -> Self {
|
||||||
|
self.drag_threshold = threshold;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_dnd(&self, clipboard: &mut dyn Clipboard, bounds: Rectangle) {
|
||||||
|
let Some(content) = self.drag_content.as_ref().map(|f| f()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
iced_core::clipboard::start_dnd(
|
||||||
|
clipboard,
|
||||||
|
false,
|
||||||
|
Some(iced_core::clipboard::DndSource::Widget(self.id.clone())),
|
||||||
|
self.drag_icon.as_ref().map(|f| {
|
||||||
|
let (icon, state) = f();
|
||||||
|
(
|
||||||
|
container(icon)
|
||||||
|
.width(Length::Fixed(bounds.width))
|
||||||
|
.height(Length::Fixed(bounds.height))
|
||||||
|
.into(),
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
Box::new(content),
|
||||||
|
self.action,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
'a,
|
||||||
|
Message: 'static,
|
||||||
|
AppMessage: 'static,
|
||||||
|
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
|
||||||
|
> Widget<Message, iced::Theme, iced::Renderer>
|
||||||
|
for DndSource<'a, Message, AppMessage, D>
|
||||||
|
{
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
vec![Tree::new(&self.container)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag(&self) -> iced_core::widget::tree::Tag {
|
||||||
|
tree::Tag::of::<State>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&mut self, tree: &mut Tree) {
|
||||||
|
tree.children[0].diff(self.container.as_widget_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> iced_core::widget::tree::State {
|
||||||
|
tree::State::new(State::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> iced_core::Size<Length> {
|
||||||
|
self.container.as_widget().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
let node = self.container.as_widget().layout(
|
||||||
|
&mut tree.children[0],
|
||||||
|
renderer,
|
||||||
|
limits,
|
||||||
|
);
|
||||||
|
state.cached_bounds = node.bounds();
|
||||||
|
node
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: layout::Layout<'_>,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
operation: &mut dyn iced_core::widget::Operation<()>,
|
||||||
|
) {
|
||||||
|
operation.custom((&mut tree.state) as &mut dyn Any, Some(&self.id));
|
||||||
|
operation.container(
|
||||||
|
Some(&self.id),
|
||||||
|
layout.bounds(),
|
||||||
|
&mut |operation| {
|
||||||
|
self.container.as_widget().operate(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: Event,
|
||||||
|
layout: layout::Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) -> event::Status {
|
||||||
|
let ret = self.container.as_widget_mut().on_event(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event.clone(),
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Mouse(mouse_event) => match mouse_event {
|
||||||
|
mouse::Event::ButtonPressed(mouse::Button::Left) => {
|
||||||
|
if let Some(position) = cursor.position() {
|
||||||
|
if !state.hovered {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.left_pressed_position = Some(position);
|
||||||
|
// dbg!(&state, &self.id);
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mouse::Event::ButtonReleased(mouse::Button::Left)
|
||||||
|
if state.left_pressed_position.is_some() =>
|
||||||
|
{
|
||||||
|
state.left_pressed_position = None;
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
mouse::Event::CursorMoved { .. } => {
|
||||||
|
if let Some(position) = cursor.position() {
|
||||||
|
if state.hovered {
|
||||||
|
// We ignore motion if we do not possess drag content by now.
|
||||||
|
if self.drag_content.is_none() {
|
||||||
|
state.left_pressed_position = None;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
if let Some(left_pressed_position) =
|
||||||
|
state.left_pressed_position
|
||||||
|
{
|
||||||
|
// dbg!(&state);
|
||||||
|
if position.distance(left_pressed_position)
|
||||||
|
> self.drag_threshold
|
||||||
|
{
|
||||||
|
self.start_dnd(
|
||||||
|
clipboard,
|
||||||
|
state.cached_bounds,
|
||||||
|
);
|
||||||
|
state.is_dragging = true;
|
||||||
|
state.left_pressed_position = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !cursor.is_over(layout.bounds()) {
|
||||||
|
state.hovered = false;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
} else if cursor.is_over(layout.bounds()) {
|
||||||
|
state.hovered = true;
|
||||||
|
}
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return ret,
|
||||||
|
},
|
||||||
|
Event::Dnd(DndEvent::Source(
|
||||||
|
SourceEvent::Cancelled | SourceEvent::Finished,
|
||||||
|
)) => {
|
||||||
|
if state.is_dragging {
|
||||||
|
state.is_dragging = false;
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
_ => return ret,
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: layout::Layout<'_>,
|
||||||
|
cursor_position: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let state = tree.state.downcast_ref::<State>();
|
||||||
|
if state.is_dragging {
|
||||||
|
return mouse::Interaction::Grabbing;
|
||||||
|
}
|
||||||
|
self.container.as_widget().mouse_interaction(
|
||||||
|
&tree.children[0],
|
||||||
|
layout,
|
||||||
|
cursor_position,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut iced::Renderer,
|
||||||
|
theme: &iced::Theme,
|
||||||
|
renderer_style: &renderer::Style,
|
||||||
|
layout: layout::Layout<'_>,
|
||||||
|
cursor_position: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.container.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
renderer_style,
|
||||||
|
layout,
|
||||||
|
cursor_position,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
translation: iced::Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, iced::Theme, iced::Renderer>>
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drag_destinations(
|
||||||
|
&self,
|
||||||
|
state: &Tree,
|
||||||
|
layout: layout::Layout<'_>,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
|
||||||
|
) {
|
||||||
|
self.container.as_widget().drag_destinations(
|
||||||
|
&state.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
dnd_rectangles,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<Id> {
|
||||||
|
Some(self.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_id(&mut self, id: Id) {
|
||||||
|
self.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
'a,
|
||||||
|
Message: 'static,
|
||||||
|
AppMessage: 'static,
|
||||||
|
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
|
||||||
|
> From<DndSource<'a, Message, AppMessage, D>> for Element<'a, Message>
|
||||||
|
{
|
||||||
|
fn from(e: DndSource<'a, Message, AppMessage, D>) -> Element<'a, Message> {
|
||||||
|
Element::new(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Local state of the [`MouseListener`].
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct State {
|
||||||
|
hovered: bool,
|
||||||
|
left_pressed_position: Option<Point>,
|
||||||
|
is_dragging: bool,
|
||||||
|
cached_bounds: Rectangle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
202
examples/sctk_drag/src/main.rs
Normal file
202
examples/sctk_drag/src/main.rs
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
mod dnd_destination;
|
||||||
|
mod dnd_source;
|
||||||
|
|
||||||
|
use std::{borrow::Cow, convert::Infallible};
|
||||||
|
|
||||||
|
use dnd_destination::dnd_destination;
|
||||||
|
use iced::{
|
||||||
|
clipboard::mime::{AllowedMimeTypes, AsMimeTypes},
|
||||||
|
platform_specific::{
|
||||||
|
runtime::wayland::layer_surface::SctkLayerSurfaceSettings,
|
||||||
|
shell::commands::layer_surface::get_layer_surface,
|
||||||
|
},
|
||||||
|
widget::{column, container, text},
|
||||||
|
window, Element, Length, Task,
|
||||||
|
};
|
||||||
|
use iced_core::{
|
||||||
|
widget::{tree, Text},
|
||||||
|
Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> iced::Result {
|
||||||
|
iced::daemon(DndTest::title, DndTest::update, DndTest::view)
|
||||||
|
.run_with(DndTest::new)
|
||||||
|
// iced::application(Todos::title, Todos::update, Todos::view)
|
||||||
|
// .subscription(Todos::subscription)
|
||||||
|
// .font(include_bytes!("../fonts/icons.ttf").as_slice())
|
||||||
|
// .window_size((500.0, 800.0))
|
||||||
|
// .run_with(Todos::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUPPORTED_MIME_TYPES: &'static [&'static str; 6] = &[
|
||||||
|
"text/plain;charset=utf-8",
|
||||||
|
"text/plain;charset=UTF-8",
|
||||||
|
"UTF8_STRING",
|
||||||
|
"STRING",
|
||||||
|
"text/plain",
|
||||||
|
"TEXT",
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct MyDndString(String);
|
||||||
|
|
||||||
|
impl AllowedMimeTypes for MyDndString {
|
||||||
|
fn allowed() -> std::borrow::Cow<'static, [String]> {
|
||||||
|
std::borrow::Cow::Owned(vec![
|
||||||
|
"text/plain;charset=utf-8".to_string(),
|
||||||
|
"text/plain;charset=UTF-8".to_string(),
|
||||||
|
"UTF8_STRING".to_string(),
|
||||||
|
"STRING".to_string(),
|
||||||
|
"text/plain".to_string(),
|
||||||
|
"TEXT".to_string(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<(Vec<u8>, String)> for MyDndString {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn try_from(value: (Vec<u8>, String)) -> Result<Self, Self::Error> {
|
||||||
|
Ok(MyDndString(
|
||||||
|
String::from_utf8_lossy(value.0.as_slice()).to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsMimeTypes for MyDndString {
|
||||||
|
fn available(&self) -> std::borrow::Cow<'static, [String]> {
|
||||||
|
std::borrow::Cow::Owned(vec![
|
||||||
|
"text/plain;charset=utf-8".to_string(),
|
||||||
|
"text/plain;charset=UTF-8".to_string(),
|
||||||
|
"UTF8_STRING".to_string(),
|
||||||
|
"STRING".to_string(),
|
||||||
|
"text/plain".to_string(),
|
||||||
|
"TEXT".to_string(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_bytes(
|
||||||
|
&self,
|
||||||
|
_mime_type: &str,
|
||||||
|
) -> Option<std::borrow::Cow<'static, [u8]>> {
|
||||||
|
Some(Cow::Owned(self.0.clone().into_bytes()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DndTest {
|
||||||
|
/// option with the dragged text
|
||||||
|
source: Option<String>,
|
||||||
|
/// is the dnd over the target
|
||||||
|
current_text: String,
|
||||||
|
/// main id
|
||||||
|
id: iced_core::window::Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Message {
|
||||||
|
Drag,
|
||||||
|
DndData(MyDndString),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DndTest {
|
||||||
|
fn new() -> (DndTest, Task<Message>) {
|
||||||
|
let current_text = String::from("Hello, world!");
|
||||||
|
let mut s = SctkLayerSurfaceSettings::default();
|
||||||
|
s.size_limits = s.size_limits.min_width(100.0).max_width(400.0);
|
||||||
|
s.size = Some((Some(500), Some(600)));
|
||||||
|
// s.anchor = Anchor::TOP.union(Anchor::BOTTOM);
|
||||||
|
(
|
||||||
|
DndTest {
|
||||||
|
current_text,
|
||||||
|
source: None,
|
||||||
|
id: iced_core::window::Id::unique(),
|
||||||
|
},
|
||||||
|
get_layer_surface(s),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self, _id: window::Id) -> String {
|
||||||
|
String::from("DndTest")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
|
match message {
|
||||||
|
Message::DndData(s) => {
|
||||||
|
dbg!(&s);
|
||||||
|
self.current_text = s.0;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, _id: window::Id) -> Element<Message> {
|
||||||
|
let s = self.current_text.chars().rev().collect::<String>();
|
||||||
|
let s2 = s.clone();
|
||||||
|
column![
|
||||||
|
dnd_destination::dnd_destination_for_data::<MyDndString, Message>(
|
||||||
|
container(text(format!(
|
||||||
|
"Drag text here: {}",
|
||||||
|
&self.current_text
|
||||||
|
)))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::FillPortion(1))
|
||||||
|
.padding(20),
|
||||||
|
|data, _| {
|
||||||
|
dbg!("got data");
|
||||||
|
Message::DndData(data.unwrap_or_default())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.drag_id(1)
|
||||||
|
.on_enter(|_, _, m| {
|
||||||
|
dbg!(m);
|
||||||
|
Message::Drag
|
||||||
|
})
|
||||||
|
.on_action_selected(|a| {
|
||||||
|
dbg!(a);
|
||||||
|
Message::Drag
|
||||||
|
})
|
||||||
|
.on_drop(|_, _| {
|
||||||
|
dbg!("drop");
|
||||||
|
Message::Drag
|
||||||
|
})
|
||||||
|
.on_motion(|x, y| {
|
||||||
|
dbg!(x, y);
|
||||||
|
Message::Drag
|
||||||
|
}),
|
||||||
|
dnd_source::dnd_source(
|
||||||
|
container(text(format!(
|
||||||
|
"Drag me: {}",
|
||||||
|
&self.current_text.chars().rev().collect::<String>()
|
||||||
|
)))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::FillPortion(1))
|
||||||
|
.padding(20)
|
||||||
|
)
|
||||||
|
.drag_threshold(5.0)
|
||||||
|
.drag_icon(move || {
|
||||||
|
let t: Text<'static, iced::Theme, iced::Renderer> =
|
||||||
|
text(s.clone());
|
||||||
|
let state = <iced_core::widget::Text<
|
||||||
|
'static,
|
||||||
|
iced::Theme,
|
||||||
|
iced::Renderer,
|
||||||
|
> as iced_core::Widget<(), iced::Theme, iced::Renderer>>::state(
|
||||||
|
&t,
|
||||||
|
);
|
||||||
|
(
|
||||||
|
Element::<'static, (), iced::Theme, iced::Renderer>::from(
|
||||||
|
t,
|
||||||
|
),
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.drag_content(move || { MyDndString(s2.clone()) })
|
||||||
|
]
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CustomTheme;
|
||||||
17
examples/sctk_lazy/Cargo.toml
Normal file
17
examples/sctk_lazy/Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "sctk_lazy"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Nick Senger <dev@nsenger.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iced = { path = "../..", features = [
|
||||||
|
"debug",
|
||||||
|
"lazy",
|
||||||
|
"wayland",
|
||||||
|
"winit",
|
||||||
|
"tokio",
|
||||||
|
"tiny-skia",
|
||||||
|
"advanced",
|
||||||
|
], default-features = false }
|
||||||
255
examples/sctk_lazy/src/main.rs
Normal file
255
examples/sctk_lazy/src/main.rs
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
use iced::advanced::layout::Limits;
|
||||||
|
use iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings;
|
||||||
|
use iced::platform_specific::shell::commands::layer_surface::{
|
||||||
|
get_layer_surface, KeyboardInteractivity,
|
||||||
|
};
|
||||||
|
|
||||||
|
use iced::widget::{
|
||||||
|
button, column, horizontal_space, lazy, pick_list, row, scrollable, text,
|
||||||
|
text_input,
|
||||||
|
};
|
||||||
|
use iced::window::Id;
|
||||||
|
use iced::Task;
|
||||||
|
use iced::{Element, Length};
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
|
pub fn main() -> iced::Result {
|
||||||
|
iced::daemon(App::title, App::update, App::view).run_with(App::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
version: u8,
|
||||||
|
items: HashSet<Item>,
|
||||||
|
input: String,
|
||||||
|
order: Order,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for App {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
version: 0,
|
||||||
|
items: ["Foo", "Bar", "Baz", "Qux", "Corge", "Waldo", "Fred"]
|
||||||
|
.into_iter()
|
||||||
|
.map(From::from)
|
||||||
|
.collect(),
|
||||||
|
input: Default::default(),
|
||||||
|
order: Order::Ascending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
enum Color {
|
||||||
|
#[default]
|
||||||
|
Black,
|
||||||
|
Red,
|
||||||
|
Orange,
|
||||||
|
Yellow,
|
||||||
|
Green,
|
||||||
|
Blue,
|
||||||
|
Purple,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
const ALL: &'static [Color] = &[
|
||||||
|
Color::Black,
|
||||||
|
Color::Red,
|
||||||
|
Color::Orange,
|
||||||
|
Color::Yellow,
|
||||||
|
Color::Green,
|
||||||
|
Color::Blue,
|
||||||
|
Color::Purple,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Color {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::Black => "Black",
|
||||||
|
Self::Red => "Red",
|
||||||
|
Self::Orange => "Orange",
|
||||||
|
Self::Yellow => "Yellow",
|
||||||
|
Self::Green => "Green",
|
||||||
|
Self::Blue => "Blue",
|
||||||
|
Self::Purple => "Purple",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Color> for iced::Color {
|
||||||
|
fn from(value: Color) -> Self {
|
||||||
|
match value {
|
||||||
|
Color::Black => iced::Color::from_rgb8(0, 0, 0),
|
||||||
|
Color::Red => iced::Color::from_rgb8(220, 50, 47),
|
||||||
|
Color::Orange => iced::Color::from_rgb8(203, 75, 22),
|
||||||
|
Color::Yellow => iced::Color::from_rgb8(181, 137, 0),
|
||||||
|
Color::Green => iced::Color::from_rgb8(133, 153, 0),
|
||||||
|
Color::Blue => iced::Color::from_rgb8(38, 139, 210),
|
||||||
|
Color::Purple => iced::Color::from_rgb8(108, 113, 196),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq)]
|
||||||
|
struct Item {
|
||||||
|
name: String,
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for Item {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.name.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Item {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.name.eq(&other.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Item {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
name: s.to_owned(),
|
||||||
|
color: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Message {
|
||||||
|
InputChanged(String),
|
||||||
|
ToggleOrder,
|
||||||
|
DeleteItem(Item),
|
||||||
|
AddItem(String),
|
||||||
|
ItemColorChanged(Item, Color),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn new() -> (App, Task<Message>) {
|
||||||
|
let mut initial_surface = SctkLayerSurfaceSettings::default();
|
||||||
|
initial_surface.keyboard_interactivity =
|
||||||
|
KeyboardInteractivity::OnDemand;
|
||||||
|
initial_surface.size_limits = Limits::NONE
|
||||||
|
.min_width(1.0)
|
||||||
|
.min_height(1.0)
|
||||||
|
.max_height(500.0)
|
||||||
|
.max_width(900.0);
|
||||||
|
initial_surface.size = Some((Some(500), Some(500)));
|
||||||
|
(Self::default(), get_layer_surface(initial_surface))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self, _id: Id) -> String {
|
||||||
|
String::from("Lazy - Iced")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) {
|
||||||
|
match message {
|
||||||
|
Message::InputChanged(input) => {
|
||||||
|
self.input = input;
|
||||||
|
}
|
||||||
|
Message::ToggleOrder => {
|
||||||
|
self.version = self.version.wrapping_add(1);
|
||||||
|
self.order = match self.order {
|
||||||
|
Order::Ascending => Order::Descending,
|
||||||
|
Order::Descending => Order::Ascending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::AddItem(name) => {
|
||||||
|
self.version = self.version.wrapping_add(1);
|
||||||
|
self.items.insert(name.as_str().into());
|
||||||
|
self.input.clear();
|
||||||
|
}
|
||||||
|
Message::DeleteItem(item) => {
|
||||||
|
self.version = self.version.wrapping_add(1);
|
||||||
|
self.items.remove(&item);
|
||||||
|
}
|
||||||
|
Message::ItemColorChanged(item, color) => {
|
||||||
|
self.version = self.version.wrapping_add(1);
|
||||||
|
if self.items.remove(&item) {
|
||||||
|
self.items.insert(Item {
|
||||||
|
name: item.name,
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, _: Id) -> Element<Message> {
|
||||||
|
let options = lazy(self.version, |_| {
|
||||||
|
let mut items: Vec<_> = self.items.iter().cloned().collect();
|
||||||
|
|
||||||
|
items.sort_by(|a, b| match self.order {
|
||||||
|
Order::Ascending => {
|
||||||
|
a.name.to_lowercase().cmp(&b.name.to_lowercase())
|
||||||
|
}
|
||||||
|
Order::Descending => {
|
||||||
|
b.name.to_lowercase().cmp(&a.name.to_lowercase())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
column(items.into_iter().map(|item| {
|
||||||
|
let button = button("Delete")
|
||||||
|
.on_press(Message::DeleteItem(item.clone()));
|
||||||
|
|
||||||
|
row![
|
||||||
|
text(item.name.clone()),
|
||||||
|
horizontal_space(),
|
||||||
|
pick_list(Color::ALL, Some(item.color), move |color| {
|
||||||
|
Message::ItemColorChanged(item.clone(), color)
|
||||||
|
}),
|
||||||
|
button
|
||||||
|
]
|
||||||
|
.spacing(20)
|
||||||
|
.into()
|
||||||
|
}))
|
||||||
|
.spacing(10)
|
||||||
|
});
|
||||||
|
|
||||||
|
column![
|
||||||
|
scrollable(options).height(Length::Fill),
|
||||||
|
row![
|
||||||
|
text_input("Add a new option", &self.input)
|
||||||
|
.on_input(Message::InputChanged)
|
||||||
|
.on_submit(Message::AddItem(self.input.clone())),
|
||||||
|
button(text(format!("Toggle Order ({})", self.order)))
|
||||||
|
.on_press(Message::ToggleOrder)
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
]
|
||||||
|
.spacing(20)
|
||||||
|
.padding(20)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn theme(&self) -> iced::Theme {
|
||||||
|
iced::Theme::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scale_factor(&self) -> f64 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Hash)]
|
||||||
|
enum Order {
|
||||||
|
Ascending,
|
||||||
|
Descending,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Order {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Self::Ascending => "Ascending",
|
||||||
|
Self::Descending => "Descending",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
examples/sctk_session_lock/Cargo.toml
Normal file
18
examples/sctk_session_lock/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "sctk_session_lock"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "828b1eb" }
|
||||||
|
iced = { path = "../..", default-features = false, features = [
|
||||||
|
"async-std",
|
||||||
|
"winit",
|
||||||
|
"wayland",
|
||||||
|
"debug",
|
||||||
|
"tiny-skia",
|
||||||
|
# "a11y",
|
||||||
|
] }
|
||||||
|
iced_runtime = { path = "../../runtime" }
|
||||||
|
env_logger = "0.10"
|
||||||
|
async-std = "1"
|
||||||
96
examples/sctk_session_lock/src/main.rs
Normal file
96
examples/sctk_session_lock/src/main.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
use iced::event::listen_raw;
|
||||||
|
use iced::Task;
|
||||||
|
use iced::{
|
||||||
|
event::wayland::{Event as WaylandEvent, OutputEvent, SessionLockEvent},
|
||||||
|
platform_specific::shell::commands::session_lock,
|
||||||
|
widget::text,
|
||||||
|
window, Element, Subscription,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> iced::Result {
|
||||||
|
iced::daemon(Locker::title, Locker::update, Locker::view)
|
||||||
|
.subscription(Locker::subscription)
|
||||||
|
.run_with(Locker::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct Locker {
|
||||||
|
_exit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Message {
|
||||||
|
WaylandEvent(WaylandEvent),
|
||||||
|
TimeUp,
|
||||||
|
Ignore,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Locker {
|
||||||
|
fn new() -> (Locker, Task<Message>) {
|
||||||
|
(
|
||||||
|
Locker {
|
||||||
|
..Locker::default()
|
||||||
|
},
|
||||||
|
session_lock::lock(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self, _id: window::Id) -> String {
|
||||||
|
String::from("Locker")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
|
match message {
|
||||||
|
Message::WaylandEvent(evt) => match evt {
|
||||||
|
WaylandEvent::Output(evt, output) => match evt {
|
||||||
|
OutputEvent::Created(_) => {
|
||||||
|
return session_lock::get_lock_surface(
|
||||||
|
window::Id::unique(),
|
||||||
|
output,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
OutputEvent::Removed => {}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
WaylandEvent::SessionLock(evt) => match evt {
|
||||||
|
SessionLockEvent::Locked => {
|
||||||
|
return iced::Task::perform(
|
||||||
|
async_std::task::sleep(
|
||||||
|
std::time::Duration::from_secs(5),
|
||||||
|
),
|
||||||
|
|_| Message::TimeUp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
SessionLockEvent::Unlocked => {
|
||||||
|
// Server has processed unlock, so it's safe to exit
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Message::TimeUp => {
|
||||||
|
return session_lock::unlock();
|
||||||
|
}
|
||||||
|
Message::Ignore => {}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, id: window::Id) -> Element<Message> {
|
||||||
|
text(format!("Lock Surface {:?}", id)).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
|
listen_raw(|evt, _, _| {
|
||||||
|
if let iced::Event::PlatformSpecific(
|
||||||
|
iced::event::PlatformSpecific::Wayland(evt),
|
||||||
|
) = evt
|
||||||
|
{
|
||||||
|
Some(Message::WaylandEvent(evt))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
19
examples/sctk_subsurface/Cargo.toml
Normal file
19
examples/sctk_subsurface/Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "sctk_subsurface"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "828b1eb" }
|
||||||
|
iced = { path = "../..", default-features = false, features = [
|
||||||
|
"tokio",
|
||||||
|
"wayland",
|
||||||
|
"winit",
|
||||||
|
"debug",
|
||||||
|
"tiny-skia",
|
||||||
|
] }
|
||||||
|
iced_runtime = { path = "../../runtime" }
|
||||||
|
env_logger = "0.10"
|
||||||
|
futures-channel = "0.3.29"
|
||||||
|
calloop = "0.12.3"
|
||||||
|
rustix = { version = "0.38.30", features = ["fs", "shm"] }
|
||||||
123
examples/sctk_subsurface/src/main.rs
Normal file
123
examples/sctk_subsurface/src/main.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
// Shows a subsurface with a 1x1 px red buffer, stretch to window size
|
||||||
|
|
||||||
|
use iced::{
|
||||||
|
event::wayland::Event as WaylandEvent,
|
||||||
|
platform_specific::shell::subsurface_widget::{self, SubsurfaceBuffer},
|
||||||
|
widget::text,
|
||||||
|
window::{self, Id, Settings},
|
||||||
|
Element, Length, Subscription, Task,
|
||||||
|
};
|
||||||
|
use sctk::reexports::client::{Connection, Proxy};
|
||||||
|
|
||||||
|
mod wayland;
|
||||||
|
|
||||||
|
fn main() -> iced::Result {
|
||||||
|
iced::daemon(
|
||||||
|
SubsurfaceApp::title,
|
||||||
|
SubsurfaceApp::update,
|
||||||
|
SubsurfaceApp::view,
|
||||||
|
)
|
||||||
|
.subscription(SubsurfaceApp::subscription)
|
||||||
|
.run_with(SubsurfaceApp::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct SubsurfaceApp {
|
||||||
|
connection: Option<Connection>,
|
||||||
|
red_buffer: Option<SubsurfaceBuffer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Message {
|
||||||
|
WaylandEvent(WaylandEvent),
|
||||||
|
Wayland(wayland::Event),
|
||||||
|
Pressed(&'static str),
|
||||||
|
Id(Id),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubsurfaceApp {
|
||||||
|
fn new() -> (SubsurfaceApp, Task<Message>) {
|
||||||
|
(
|
||||||
|
SubsurfaceApp {
|
||||||
|
..SubsurfaceApp::default()
|
||||||
|
},
|
||||||
|
iced::window::open(Settings {
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.1
|
||||||
|
.map(Message::Id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self, _id: window::Id) -> String {
|
||||||
|
String::from("SubsurfaceApp")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
|
match message {
|
||||||
|
Message::WaylandEvent(evt) => match evt {
|
||||||
|
WaylandEvent::Output(_evt, output) => {
|
||||||
|
if self.connection.is_none() {
|
||||||
|
if let Some(backend) = output.backend().upgrade() {
|
||||||
|
self.connection =
|
||||||
|
Some(Connection::from_backend(backend));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Message::Wayland(evt) => match evt {
|
||||||
|
wayland::Event::RedBuffer(buffer) => {
|
||||||
|
self.red_buffer = Some(buffer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Message::Pressed(side) => println!("{side} surface pressed"),
|
||||||
|
Message::Id(_) => {}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, _id: window::Id) -> Element<Message> {
|
||||||
|
if let Some(buffer) = &self.red_buffer {
|
||||||
|
iced::widget::row![
|
||||||
|
iced::widget::button(
|
||||||
|
subsurface_widget::Subsurface::new(1, 1, buffer)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.on_press(Message::Pressed("left")),
|
||||||
|
iced::widget::button(
|
||||||
|
subsurface_widget::Subsurface::new(1, 1, buffer)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.on_press(Message::Pressed("right"))
|
||||||
|
]
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
text("No subsurface").into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
|
let mut subscriptions = vec![iced::event::listen_with(|evt, _, _| {
|
||||||
|
if let iced::Event::PlatformSpecific(
|
||||||
|
iced::event::PlatformSpecific::Wayland(evt),
|
||||||
|
) = evt
|
||||||
|
{
|
||||||
|
Some(Message::WaylandEvent(evt))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})];
|
||||||
|
if let Some(connection) = &self.connection {
|
||||||
|
subscriptions
|
||||||
|
.push(wayland::subscription(connection).map(Message::Wayland));
|
||||||
|
}
|
||||||
|
Subscription::batch(subscriptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
128
examples/sctk_subsurface/src/wayland.rs
Normal file
128
examples/sctk_subsurface/src/wayland.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
use futures_channel::mpsc;
|
||||||
|
use iced::{
|
||||||
|
futures::{FutureExt, SinkExt},
|
||||||
|
platform_specific::shell::subsurface_widget::{Shmbuf, SubsurfaceBuffer},
|
||||||
|
};
|
||||||
|
use iced_runtime::futures::subscription;
|
||||||
|
use rustix::{io::Errno, shm::ShmOFlags};
|
||||||
|
use sctk::{
|
||||||
|
reexports::{
|
||||||
|
calloop_wayland_source::WaylandSource,
|
||||||
|
client::{
|
||||||
|
delegate_noop,
|
||||||
|
globals::registry_queue_init,
|
||||||
|
protocol::{wl_buffer::WlBuffer, wl_shm},
|
||||||
|
Connection,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
registry::{ProvidesRegistryState, RegistryState},
|
||||||
|
shm::{Shm, ShmHandler},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
os::fd::OwnedFd,
|
||||||
|
sync::Arc,
|
||||||
|
thread,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Event {
|
||||||
|
RedBuffer(SubsurfaceBuffer),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppData {
|
||||||
|
registry_state: RegistryState,
|
||||||
|
shm_state: Shm,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProvidesRegistryState for AppData {
|
||||||
|
fn registry(&mut self) -> &mut RegistryState {
|
||||||
|
&mut self.registry_state
|
||||||
|
}
|
||||||
|
|
||||||
|
sctk::registry_handlers!();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShmHandler for AppData {
|
||||||
|
fn shm_state(&mut self) -> &mut Shm {
|
||||||
|
&mut self.shm_state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscription(connection: &Connection) -> iced::Subscription<Event> {
|
||||||
|
let connection = connection.clone();
|
||||||
|
subscription::Subscription::run_with_id(
|
||||||
|
"wayland-sub",
|
||||||
|
async { start(connection).await }.flatten_stream(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start(conn: Connection) -> mpsc::Receiver<Event> {
|
||||||
|
let (mut sender, receiver) = mpsc::channel(20);
|
||||||
|
|
||||||
|
let (globals, event_queue) = registry_queue_init(&conn).unwrap();
|
||||||
|
let qh = event_queue.handle();
|
||||||
|
|
||||||
|
let mut app_data = AppData {
|
||||||
|
registry_state: RegistryState::new(&globals),
|
||||||
|
shm_state: Shm::bind(&globals, &qh).unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let fd = create_memfile().unwrap();
|
||||||
|
rustix::io::write(&fd, &[0, 255, 0, 255]).unwrap();
|
||||||
|
|
||||||
|
let shmbuf = Shmbuf {
|
||||||
|
fd,
|
||||||
|
offset: 0,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
stride: 4,
|
||||||
|
format: wl_shm::Format::Xrgb8888,
|
||||||
|
};
|
||||||
|
|
||||||
|
let buffer = SubsurfaceBuffer::new(Arc::new(shmbuf.into())).0;
|
||||||
|
let _ = sender.send(Event::RedBuffer(buffer)).await;
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut event_loop = calloop::EventLoop::try_new().unwrap();
|
||||||
|
WaylandSource::new(conn, event_queue)
|
||||||
|
.insert(event_loop.handle())
|
||||||
|
.unwrap();
|
||||||
|
loop {
|
||||||
|
event_loop.dispatch(None, &mut app_data).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
receiver
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_memfile() -> rustix::io::Result<OwnedFd> {
|
||||||
|
loop {
|
||||||
|
let flags = ShmOFlags::CREATE | ShmOFlags::EXCL | ShmOFlags::RDWR;
|
||||||
|
|
||||||
|
let time = SystemTime::now();
|
||||||
|
let name = format!(
|
||||||
|
"/iced-sctk-{}",
|
||||||
|
time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos()
|
||||||
|
);
|
||||||
|
|
||||||
|
match rustix::io::retry_on_intr(|| {
|
||||||
|
rustix::shm::shm_open(&name, flags, 0600.into())
|
||||||
|
}) {
|
||||||
|
Ok(fd) => match rustix::shm::shm_unlink(&name) {
|
||||||
|
Ok(_) => return Ok(fd),
|
||||||
|
Err(errno) => {
|
||||||
|
return Err(errno.into());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(Errno::EXIST) => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate_noop!(AppData: ignore WlBuffer);
|
||||||
|
sctk::delegate_registry!(AppData);
|
||||||
|
sctk::delegate_shm!(AppData);
|
||||||
21
examples/sctk_subsurface_gst/Cargo.toml
Normal file
21
examples/sctk_subsurface_gst/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "sctk_subsurface_gst"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "828b1eb" }
|
||||||
|
iced = { path = "../..", default-features = false, features = [
|
||||||
|
"wayland",
|
||||||
|
"debug",
|
||||||
|
"a11y",
|
||||||
|
] }
|
||||||
|
iced_runtime = { path = "../../runtime" }
|
||||||
|
env_logger = "0.10"
|
||||||
|
futures-channel = "0.3.29"
|
||||||
|
calloop = "0.12.3"
|
||||||
|
gst = { package = "gstreamer", version = "0.21.3" }
|
||||||
|
gst-app = { package = "gstreamer-app", version = "0.21.2" }
|
||||||
|
gst-video = { package = "gstreamer-video", version = "0.21.2" }
|
||||||
|
gst-allocators = { package = "gstreamer-allocators", version = "0.21.2" }
|
||||||
|
drm-fourcc = "2.2.0"
|
||||||
84
examples/sctk_subsurface_gst/src/main.rs
Normal file
84
examples/sctk_subsurface_gst/src/main.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
// Shows a subsurface with a 1x1 px red buffer, stretch to window size
|
||||||
|
|
||||||
|
use iced::{
|
||||||
|
wayland::InitialSurface, widget::text, window, Application, Command,
|
||||||
|
Element, Length, Subscription, Theme,
|
||||||
|
};
|
||||||
|
use iced_sctk::subsurface_widget::SubsurfaceBuffer;
|
||||||
|
use std::{env, path::Path};
|
||||||
|
|
||||||
|
mod pipewire;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = env::args();
|
||||||
|
if args.len() != 2 {
|
||||||
|
eprintln!("usage: sctk_subsurface_gst [h264 mp4 path]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let path = args.skip(1).next().unwrap();
|
||||||
|
if !Path::new(&path).exists() {
|
||||||
|
eprintln!("File `{path}` not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut settings = iced::Settings::with_flags(path);
|
||||||
|
settings.initial_surface = InitialSurface::XdgWindow(Default::default());
|
||||||
|
SubsurfaceApp::run(settings).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct SubsurfaceApp {
|
||||||
|
path: String,
|
||||||
|
buffer: Option<SubsurfaceBuffer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Message {
|
||||||
|
Pipewire(pipewire::Event),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Application for SubsurfaceApp {
|
||||||
|
type Executor = iced::executor::Default;
|
||||||
|
type Message = Message;
|
||||||
|
type Flags = String;
|
||||||
|
type Theme = Theme;
|
||||||
|
|
||||||
|
fn new(flags: String) -> (SubsurfaceApp, Command<Self::Message>) {
|
||||||
|
(
|
||||||
|
SubsurfaceApp {
|
||||||
|
path: flags,
|
||||||
|
..SubsurfaceApp::default()
|
||||||
|
},
|
||||||
|
Command::none(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self, _id: window::Id) -> String {
|
||||||
|
String::from("SubsurfaceApp")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
||||||
|
match message {
|
||||||
|
Message::Pipewire(evt) => match evt {
|
||||||
|
pipewire::Event::Frame(subsurface_buffer) => {
|
||||||
|
self.buffer = Some(subsurface_buffer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, _id: window::Id) -> Element<Self::Message> {
|
||||||
|
if let Some(buffer) = &self.buffer {
|
||||||
|
iced_sctk::subsurface_widget::Subsurface::new(1, 1, buffer)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
text("No subsurface").into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> Subscription<Self::Message> {
|
||||||
|
pipewire::subscription(&self.path).map(Message::Pipewire)
|
||||||
|
}
|
||||||
|
}
|
||||||
185
examples/sctk_subsurface_gst/src/pipewire.rs
Normal file
185
examples/sctk_subsurface_gst/src/pipewire.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
use drm_fourcc::{DrmFourcc, DrmModifier};
|
||||||
|
use gst::glib::{self, translate::IntoGlib};
|
||||||
|
use gst::prelude::*;
|
||||||
|
use iced::futures::{executor::block_on, SinkExt};
|
||||||
|
use iced_sctk::subsurface_widget::{
|
||||||
|
BufferSource, Dmabuf, Plane, SubsurfaceBuffer,
|
||||||
|
};
|
||||||
|
use std::{ffi::c_void, os::unix::io::BorrowedFd, sync::Arc, thread};
|
||||||
|
|
||||||
|
const USE_NV12: bool = false;
|
||||||
|
|
||||||
|
// Store a reference to the `BufferSource` in the data assocaited with the `BufferRef`.
|
||||||
|
// So the `BufferSource` can be re-used, instead of dupping fds and creating a new
|
||||||
|
// `wl_buffer` each buffer swap.
|
||||||
|
//
|
||||||
|
// See https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/main/subprojects/gst-plugins-bad/gst-libs/gst/wayland/gstwlbuffer.c
|
||||||
|
// for information about how `waylandsink` does this.
|
||||||
|
fn get_buffer_source(buffer: &gst::BufferRef) -> Option<Arc<BufferSource>> {
|
||||||
|
let buffer_source_quark = glib::Quark::from_str("SctkBufferSource");
|
||||||
|
unsafe {
|
||||||
|
let data = gst::ffi::gst_mini_object_get_qdata(
|
||||||
|
buffer.upcast_ref().as_mut_ptr(),
|
||||||
|
buffer_source_quark.into_glib(),
|
||||||
|
);
|
||||||
|
if data.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Arc::increment_strong_count(data as *const BufferSource);
|
||||||
|
Some(Arc::from_raw(data as *const BufferSource))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_buffer_source(buffer: &gst::BufferRef, source: Arc<BufferSource>) {
|
||||||
|
let buffer_source_quark = glib::Quark::from_str("SctkBufferSource");
|
||||||
|
unsafe extern "C" fn destroy_buffer_source(data: *mut c_void) {
|
||||||
|
Arc::from_raw(data);
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
gst::ffi::gst_mini_object_set_qdata(
|
||||||
|
buffer.upcast_ref().as_mut_ptr(),
|
||||||
|
buffer_source_quark.into_glib(),
|
||||||
|
Arc::into_raw(source) as *mut c_void,
|
||||||
|
Some(destroy_buffer_source),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Event {
|
||||||
|
Frame(SubsurfaceBuffer),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscription(path: &str) -> iced::Subscription<Event> {
|
||||||
|
let path = path.to_string();
|
||||||
|
iced::subscription::channel("pw", 16, |sender| async {
|
||||||
|
thread::spawn(move || pipewire_thread(&path, sender));
|
||||||
|
std::future::pending().await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pipewire_thread(
|
||||||
|
path: &str,
|
||||||
|
mut sender: futures_channel::mpsc::Sender<Event>,
|
||||||
|
) {
|
||||||
|
gst::init().unwrap();
|
||||||
|
|
||||||
|
let pipeline = gst::parse_launch(&format!(
|
||||||
|
"filesrc name=filesrc !
|
||||||
|
qtdemux !
|
||||||
|
h264parse !
|
||||||
|
vah264dec !
|
||||||
|
vapostproc name=postproc !
|
||||||
|
capsfilter name=capfilter !
|
||||||
|
appsink name=sink",
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
.dynamic_cast::<gst::Pipeline>()
|
||||||
|
.unwrap();
|
||||||
|
pipeline
|
||||||
|
.by_name("filesrc")
|
||||||
|
.unwrap()
|
||||||
|
.set_property("location", path);
|
||||||
|
|
||||||
|
let format = if USE_NV12 {
|
||||||
|
/*
|
||||||
|
pipeline
|
||||||
|
.remove(&pipeline.by_name("postproc").unwrap())
|
||||||
|
.unwrap();
|
||||||
|
*/
|
||||||
|
gst_video::VideoFormat::Nv12
|
||||||
|
} else {
|
||||||
|
gst_video::VideoFormat::Bgra
|
||||||
|
};
|
||||||
|
pipeline.by_name("capfilter").unwrap().set_property(
|
||||||
|
"caps",
|
||||||
|
gst_video::VideoCapsBuilder::new()
|
||||||
|
.features(["memory:DMABuf"])
|
||||||
|
.format(format)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let appsink = pipeline
|
||||||
|
.by_name("sink")
|
||||||
|
.unwrap()
|
||||||
|
.dynamic_cast::<gst_app::AppSink>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut subsurface_release = None;
|
||||||
|
|
||||||
|
appsink.set_callbacks(
|
||||||
|
gst_app::AppSinkCallbacks::builder()
|
||||||
|
.new_sample(move |appsink| {
|
||||||
|
let sample =
|
||||||
|
appsink.pull_sample().map_err(|_| gst::FlowError::Eos)?;
|
||||||
|
|
||||||
|
let buffer = sample.buffer().unwrap();
|
||||||
|
let meta = buffer.meta::<gst_video::VideoMeta>().unwrap();
|
||||||
|
|
||||||
|
let buffer_source = if let Some(buffer_source) = get_buffer_source(buffer) {
|
||||||
|
buffer_source
|
||||||
|
} else {
|
||||||
|
let planes = (0..meta.n_planes())
|
||||||
|
.map(|plane_idx| {
|
||||||
|
let memory = buffer
|
||||||
|
.memory(plane_idx)
|
||||||
|
.unwrap()
|
||||||
|
.downcast_memory::<gst_allocators::DmaBufMemory>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// TODO avoid dup?
|
||||||
|
let fd = unsafe { BorrowedFd::borrow_raw(memory.fd()) }
|
||||||
|
.try_clone_to_owned()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Plane {
|
||||||
|
fd,
|
||||||
|
plane_idx,
|
||||||
|
offset: meta.offset()[plane_idx as usize] as u32,
|
||||||
|
stride: meta.stride()[plane_idx as usize] as u32,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let format = if USE_NV12 {
|
||||||
|
DrmFourcc::Nv12
|
||||||
|
} else {
|
||||||
|
DrmFourcc::Argb8888
|
||||||
|
};
|
||||||
|
let dmabuf = Dmabuf {
|
||||||
|
width: meta.width() as i32,
|
||||||
|
height: meta.height() as i32,
|
||||||
|
planes,
|
||||||
|
// TODO should use dmabuf protocol to get supported formats,
|
||||||
|
// convert if needed.
|
||||||
|
format: format as u32,
|
||||||
|
// TODO modifier negotiation
|
||||||
|
modifier: DrmModifier::Linear.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let buffer_source = Arc::new(BufferSource::from(dmabuf));
|
||||||
|
set_buffer_source(buffer, buffer_source.clone());
|
||||||
|
buffer_source
|
||||||
|
};
|
||||||
|
|
||||||
|
let (buffer, new_subsurface_release) =
|
||||||
|
SubsurfaceBuffer::new(buffer_source);
|
||||||
|
block_on(sender.send(Event::Frame(buffer))).unwrap();
|
||||||
|
|
||||||
|
// Wait for server to release other buffer
|
||||||
|
// TODO is gstreamer using triple buffering?
|
||||||
|
if let Some(release) = subsurface_release.take() {
|
||||||
|
block_on(release);
|
||||||
|
}
|
||||||
|
subsurface_release = Some(new_subsurface_release);
|
||||||
|
|
||||||
|
Ok(gst::FlowSuccess::Ok)
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
pipeline.set_state(gst::State::Playing).unwrap();
|
||||||
|
let bus = pipeline.bus().unwrap();
|
||||||
|
for _msg in bus.iter_timed(gst::ClockTime::NONE) {}
|
||||||
|
}
|
||||||
32
examples/sctk_todos/Cargo.toml
Normal file
32
examples/sctk_todos/Cargo.toml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
[package]
|
||||||
|
name = "sctk_todos"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iced.workspace = true
|
||||||
|
iced.features = [
|
||||||
|
"tokio",
|
||||||
|
"wayland",
|
||||||
|
"winit",
|
||||||
|
"debug",
|
||||||
|
"tiny-skia",
|
||||||
|
"a11y",
|
||||||
|
"wgpu",
|
||||||
|
]
|
||||||
|
# iced.features = ["async-std", "wayland", "debug", "wayland-clipboard", "a11y", "tiny-skia"]
|
||||||
|
# TODO(POP): Fix a11y not working with new winit
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
iced_core.workspace = true
|
||||||
|
once_cell = "1.15"
|
||||||
|
sctk.workspace = true
|
||||||
|
log = "0.4.17"
|
||||||
|
env_logger = "0.10.0"
|
||||||
|
async-std = "1.0"
|
||||||
|
directories-next = "2.0.0"
|
||||||
|
|
||||||
|
[profile.release-opt]
|
||||||
|
debug = true
|
||||||
20
examples/sctk_todos/README.md
Normal file
20
examples/sctk_todos/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
## Todos
|
||||||
|
|
||||||
|
A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them.
|
||||||
|
|
||||||
|
All the example code is located in the __[`main`]__ file.
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://gfycat.com/littlesanehalicore">
|
||||||
|
<img src="https://thumbs.gfycat.com/LittleSaneHalicore-small.gif" height="400px">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
You can run the native version with `cargo run`:
|
||||||
|
```
|
||||||
|
cargo run --package todos
|
||||||
|
```
|
||||||
|
We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_!
|
||||||
|
|
||||||
|
[`main`]: src/main.rs
|
||||||
|
[TodoMVC]: http://todomvc.com/
|
||||||
BIN
examples/sctk_todos/fonts/icons.ttf
Normal file
BIN
examples/sctk_todos/fonts/icons.ttf
Normal file
Binary file not shown.
4
examples/sctk_todos/iced-todos.desktop
Normal file
4
examples/sctk_todos/iced-todos.desktop
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=Todos - Iced
|
||||||
|
Exec=iced-todos
|
||||||
|
Type=Application
|
||||||
12
examples/sctk_todos/index.html
Normal file
12
examples/sctk_todos/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Todos - Iced</title>
|
||||||
|
<base data-trunk-public-url />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="z" data-bin="todos" />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
648
examples/sctk_todos/src/main.rs
Normal file
648
examples/sctk_todos/src/main.rs
Normal file
|
|
@ -0,0 +1,648 @@
|
||||||
|
use env_logger::Env;
|
||||||
|
use iced::alignment::{self, Alignment};
|
||||||
|
use iced::event::{self, listen_raw, Event};
|
||||||
|
use iced::platform_specific::shell::commands::layer_surface::{
|
||||||
|
get_layer_surface, Anchor,
|
||||||
|
};
|
||||||
|
use iced::theme::{self, Theme};
|
||||||
|
use iced::widget::{
|
||||||
|
self, button, checkbox, column, container, row, scrollable, text,
|
||||||
|
text_input, Text,
|
||||||
|
};
|
||||||
|
use iced::window::Settings;
|
||||||
|
use iced::{window, Application, Element, Program, Task};
|
||||||
|
use iced::{Color, Font, Length, Subscription};
|
||||||
|
use iced_core::id::Id;
|
||||||
|
use iced_core::keyboard::key::Named;
|
||||||
|
use iced_core::layout::Limits;
|
||||||
|
use iced_core::{id, keyboard};
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
static INPUT_ID: Lazy<text_input::Id> = Lazy::new(|| text_input::Id::unique());
|
||||||
|
|
||||||
|
pub fn main() -> iced::Result {
|
||||||
|
let env = Env::default()
|
||||||
|
.filter_or("MY_LOG_LEVEL", "info")
|
||||||
|
.write_style_or("MY_LOG_STYLE", "always");
|
||||||
|
|
||||||
|
env_logger::init_from_env(env);
|
||||||
|
iced::daemon(Todos::title, Todos::update, Todos::view)
|
||||||
|
.subscription(Todos::subscription)
|
||||||
|
.font(include_bytes!("../fonts/icons.ttf").as_slice())
|
||||||
|
.run_with(Todos::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Todos {
|
||||||
|
Loading,
|
||||||
|
Loaded(State),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct State {
|
||||||
|
window_id_ctr: u128,
|
||||||
|
input_value: String,
|
||||||
|
filter: Filter,
|
||||||
|
tasks: Vec<MyTask>,
|
||||||
|
dirty: bool,
|
||||||
|
saving: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum Message {
|
||||||
|
Loaded(Result<SavedState, LoadError>),
|
||||||
|
Saved(Result<(), SaveError>),
|
||||||
|
InputChanged(String),
|
||||||
|
CreateTask,
|
||||||
|
FilterChanged(Filter),
|
||||||
|
TaskMessage(usize, TaskMessage),
|
||||||
|
TabPressed { shift: bool },
|
||||||
|
CloseRequested(window::Id),
|
||||||
|
Ignore,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for Message {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Message::Loaded(_) => write!(f, "Message::Loaded(_)"),
|
||||||
|
Message::Saved(_) => write!(f, "Message::Saved(_)"),
|
||||||
|
Message::InputChanged(_) => write!(f, "Message::InputChanged(_)"),
|
||||||
|
Message::CreateTask => write!(f, "Message::CreateTask"),
|
||||||
|
Message::FilterChanged(_) => write!(f, "Message::FilterChanged(_)"),
|
||||||
|
Message::TaskMessage(_, _) => {
|
||||||
|
write!(f, "Message::TaskMessage(_, _)")
|
||||||
|
}
|
||||||
|
Message::TabPressed { shift: _ } => {
|
||||||
|
write!(f, "Message::TabPressed {{ shift: _ }}")
|
||||||
|
}
|
||||||
|
Message::CloseRequested(_) => {
|
||||||
|
write!(f, "Message::CloseRequested(_)")
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Ignore => write!(f, "Message::Ignore"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Todos {
|
||||||
|
fn new() -> (Todos, Task<Message>) {
|
||||||
|
(
|
||||||
|
Todos::Loading,
|
||||||
|
Task::batch(vec![
|
||||||
|
Task::perform(SavedState::load(), Message::Loaded),
|
||||||
|
get_layer_surface(iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings {
|
||||||
|
size: Some((None, Some(500))),
|
||||||
|
pointer_interactivity: true,
|
||||||
|
keyboard_interactivity: sctk::shell::wlr_layer::KeyboardInteractivity::OnDemand,
|
||||||
|
anchor: Anchor::LEFT.union(Anchor::RIGHT).union(Anchor::TOP),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self, _id: window::Id) -> String {
|
||||||
|
let dirty = match self {
|
||||||
|
Todos::Loading => false,
|
||||||
|
Todos::Loaded(state) => state.dirty,
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("Todos{} - Iced", if dirty { "*" } else { "" })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
|
match self {
|
||||||
|
Todos::Loading => {
|
||||||
|
match message {
|
||||||
|
Message::Loaded(Ok(state)) => {
|
||||||
|
*self = Todos::Loaded(State {
|
||||||
|
input_value: state.input_value,
|
||||||
|
filter: state.filter,
|
||||||
|
tasks: state.tasks,
|
||||||
|
window_id_ctr: 1,
|
||||||
|
..State::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Message::Loaded(Err(_)) => {
|
||||||
|
*self = Todos::Loaded(State::default());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
text_input::focus(INPUT_ID.clone())
|
||||||
|
}
|
||||||
|
Todos::Loaded(state) => {
|
||||||
|
let mut saved = false;
|
||||||
|
|
||||||
|
let command = match message {
|
||||||
|
Message::InputChanged(value) => {
|
||||||
|
state.input_value = value;
|
||||||
|
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::CreateTask => {
|
||||||
|
if !state.input_value.is_empty() {
|
||||||
|
state
|
||||||
|
.tasks
|
||||||
|
.push(MyTask::new(state.input_value.clone()));
|
||||||
|
state.input_value.clear();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::FilterChanged(filter) => {
|
||||||
|
state.filter = filter;
|
||||||
|
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::TaskMessage(i, TaskMessage::Delete) => {
|
||||||
|
state.tasks.remove(i);
|
||||||
|
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::TaskMessage(i, task_message) => {
|
||||||
|
if let Some(task) = state.tasks.get_mut(i) {
|
||||||
|
let should_focus =
|
||||||
|
matches!(task_message, TaskMessage::Edit);
|
||||||
|
|
||||||
|
task.update(task_message);
|
||||||
|
|
||||||
|
if should_focus {
|
||||||
|
let id = MyTask::text_input_id(i);
|
||||||
|
Task::batch(vec![
|
||||||
|
text_input::focus(INPUT_ID.clone()),
|
||||||
|
text_input::select_all(INPUT_ID.clone()),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::Saved(_) => {
|
||||||
|
state.saving = false;
|
||||||
|
saved = true;
|
||||||
|
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::TabPressed { shift } => {
|
||||||
|
if shift {
|
||||||
|
widget::focus_previous()
|
||||||
|
} else {
|
||||||
|
widget::focus_next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::CloseRequested(_) => {
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
_ => Task::none(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !saved {
|
||||||
|
state.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let save = if state.dirty && !state.saving {
|
||||||
|
state.dirty = false;
|
||||||
|
state.saving = true;
|
||||||
|
|
||||||
|
Task::perform(
|
||||||
|
SavedState {
|
||||||
|
input_value: state.input_value.clone(),
|
||||||
|
filter: state.filter,
|
||||||
|
tasks: state.tasks.clone(),
|
||||||
|
}
|
||||||
|
.save(),
|
||||||
|
Message::Saved,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
};
|
||||||
|
|
||||||
|
Task::batch(vec![command, save])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, id: window::Id) -> Element<Message> {
|
||||||
|
match self {
|
||||||
|
Todos::Loading => loading_message(),
|
||||||
|
Todos::Loaded(State {
|
||||||
|
input_value,
|
||||||
|
filter,
|
||||||
|
tasks,
|
||||||
|
window_id_ctr,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let title = text("todos")
|
||||||
|
.width(Length::Fill)
|
||||||
|
.size(100)
|
||||||
|
.color([0.5, 0.5, 0.5]);
|
||||||
|
|
||||||
|
let input = text_input("What needs to be done?", input_value)
|
||||||
|
.id(INPUT_ID.clone())
|
||||||
|
.padding(15)
|
||||||
|
.size(30)
|
||||||
|
.on_submit(Message::CreateTask)
|
||||||
|
.on_input(Message::InputChanged)
|
||||||
|
.on_paste(Message::InputChanged);
|
||||||
|
|
||||||
|
let controls = view_controls(tasks, *filter);
|
||||||
|
let filtered_tasks =
|
||||||
|
tasks.iter().filter(|task| filter.matches(task));
|
||||||
|
|
||||||
|
let tasks: Element<_> = if filtered_tasks.count() > 0 {
|
||||||
|
column(
|
||||||
|
tasks
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, task)| filter.matches(task))
|
||||||
|
.map(|(i, task)| {
|
||||||
|
task.view(i).map(move |message| {
|
||||||
|
Message::TaskMessage(i, message)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.spacing(10)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
empty_message(match filter {
|
||||||
|
Filter::All => "You have not created a task yet...",
|
||||||
|
Filter::Active => "All your tasks are done! :D",
|
||||||
|
Filter::Completed => {
|
||||||
|
"You have not completed a task yet..."
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = column![title, input, controls, tasks]
|
||||||
|
.spacing(20)
|
||||||
|
.max_width(800);
|
||||||
|
|
||||||
|
scrollable(
|
||||||
|
container(content)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding(40)
|
||||||
|
.center_x(Length::Fill),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
|
listen_raw(|event, status, window| {
|
||||||
|
// dbg!(&event);
|
||||||
|
match (event, status, window) {
|
||||||
|
(
|
||||||
|
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||||
|
key: keyboard::Key::Named(Named::Tab),
|
||||||
|
modifiers,
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
event::Status::Ignored,
|
||||||
|
_,
|
||||||
|
) => Some(Message::TabPressed {
|
||||||
|
shift: modifiers.shift(),
|
||||||
|
}),
|
||||||
|
(
|
||||||
|
Event::PlatformSpecific(event::PlatformSpecific::Wayland(
|
||||||
|
event::wayland::Event::Window(e),
|
||||||
|
)),
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
) => {
|
||||||
|
dbg!(&e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct MyTask {
|
||||||
|
description: String,
|
||||||
|
completed: bool,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
state: TaskState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TaskState {
|
||||||
|
Idle,
|
||||||
|
Editing,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TaskState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Idle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TaskMessage {
|
||||||
|
Completed(bool),
|
||||||
|
Edit,
|
||||||
|
DescriptionEdited(String),
|
||||||
|
FinishEdition,
|
||||||
|
Delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MyTask {
|
||||||
|
fn text_input_id(i: usize) -> text_input::Id {
|
||||||
|
text_input::Id::new(format!("task-{}", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(description: String) -> Self {
|
||||||
|
MyTask {
|
||||||
|
description,
|
||||||
|
completed: false,
|
||||||
|
state: TaskState::Idle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: TaskMessage) {
|
||||||
|
match message {
|
||||||
|
TaskMessage::Completed(completed) => {
|
||||||
|
self.completed = completed;
|
||||||
|
}
|
||||||
|
TaskMessage::Edit => {
|
||||||
|
self.state = TaskState::Editing;
|
||||||
|
}
|
||||||
|
TaskMessage::DescriptionEdited(new_description) => {
|
||||||
|
self.description = new_description;
|
||||||
|
}
|
||||||
|
TaskMessage::FinishEdition => {
|
||||||
|
if !self.description.is_empty() {
|
||||||
|
self.state = TaskState::Idle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TaskMessage::Delete => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, i: usize) -> Element<TaskMessage> {
|
||||||
|
match &self.state {
|
||||||
|
TaskState::Idle => {
|
||||||
|
let checkbox = checkbox(&self.description, self.completed)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.on_toggle(TaskMessage::Completed);
|
||||||
|
|
||||||
|
row![
|
||||||
|
checkbox,
|
||||||
|
button(edit_icon())
|
||||||
|
.on_press(TaskMessage::Edit)
|
||||||
|
.padding(10)
|
||||||
|
.style(button::text),
|
||||||
|
]
|
||||||
|
.spacing(20)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
TaskState::Editing => {
|
||||||
|
let text_input =
|
||||||
|
text_input("Describe your task...", &self.description)
|
||||||
|
.id(Self::text_input_id(i))
|
||||||
|
.on_submit(TaskMessage::FinishEdition)
|
||||||
|
.on_input(TaskMessage::DescriptionEdited)
|
||||||
|
.on_paste(TaskMessage::DescriptionEdited)
|
||||||
|
.padding(10);
|
||||||
|
|
||||||
|
row![
|
||||||
|
text_input,
|
||||||
|
button(row![delete_icon(), "Delete"].spacing(10))
|
||||||
|
.on_press(TaskMessage::Delete)
|
||||||
|
.padding(10)
|
||||||
|
.style(button::danger)
|
||||||
|
]
|
||||||
|
.spacing(20)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_controls(tasks: &[MyTask], current_filter: Filter) -> Element<Message> {
|
||||||
|
let tasks_left = tasks.iter().filter(|task| !task.completed).count();
|
||||||
|
|
||||||
|
let filter_button = |label, filter, current_filter| {
|
||||||
|
let label = text(label).size(16);
|
||||||
|
|
||||||
|
let button = button(label).style(if filter == current_filter {
|
||||||
|
button::primary
|
||||||
|
} else {
|
||||||
|
button::text
|
||||||
|
});
|
||||||
|
|
||||||
|
button.on_press(Message::FilterChanged(filter)).padding(8)
|
||||||
|
};
|
||||||
|
|
||||||
|
row![
|
||||||
|
text(format!(
|
||||||
|
"{} {} left",
|
||||||
|
tasks_left,
|
||||||
|
if tasks_left == 1 { "task" } else { "tasks" }
|
||||||
|
))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.size(16),
|
||||||
|
row![
|
||||||
|
filter_button("All", Filter::All, current_filter),
|
||||||
|
filter_button("Active", Filter::Active, current_filter),
|
||||||
|
filter_button("Completed", Filter::Completed, current_filter,),
|
||||||
|
]
|
||||||
|
.width(Length::Shrink)
|
||||||
|
.spacing(10)
|
||||||
|
]
|
||||||
|
.spacing(20)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Filter {
|
||||||
|
All,
|
||||||
|
Active,
|
||||||
|
Completed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Filter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Filter::All
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter {
|
||||||
|
fn matches(&self, task: &MyTask) -> bool {
|
||||||
|
match self {
|
||||||
|
Filter::All => true,
|
||||||
|
Filter::Active => !task.completed,
|
||||||
|
Filter::Completed => task.completed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn loading_message<'a>() -> Element<'a, Message> {
|
||||||
|
container(text("Loading...").size(50))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.center_y(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_message(message: &str) -> Element<'_, Message> {
|
||||||
|
container(
|
||||||
|
text(message)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.size(25)
|
||||||
|
.color([0.7, 0.7, 0.7]),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fixed(200.0))
|
||||||
|
.center_y(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonts
|
||||||
|
const ICONS: Font = Font::with_name("Iced-Todos-Icons");
|
||||||
|
|
||||||
|
fn icon(unicode: char) -> Text<'static> {
|
||||||
|
text(unicode.to_string())
|
||||||
|
.font(ICONS)
|
||||||
|
.width(Length::Fixed(20.0))
|
||||||
|
.size(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit_icon() -> Text<'static> {
|
||||||
|
icon('\u{F303}')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_icon() -> Text<'static> {
|
||||||
|
icon('\u{F1F8}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persistence
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct SavedState {
|
||||||
|
input_value: String,
|
||||||
|
filter: Filter,
|
||||||
|
tasks: Vec<MyTask>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum LoadError {
|
||||||
|
File,
|
||||||
|
Format,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum SaveError {
|
||||||
|
File,
|
||||||
|
Write,
|
||||||
|
Format,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
impl SavedState {
|
||||||
|
fn path() -> std::path::PathBuf {
|
||||||
|
let mut path = if let Some(project_dirs) =
|
||||||
|
directories_next::ProjectDirs::from("rs", "Iced", "Todos")
|
||||||
|
{
|
||||||
|
project_dirs.data_dir().into()
|
||||||
|
} else {
|
||||||
|
std::env::current_dir().unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
path.push("todos.json");
|
||||||
|
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load() -> Result<SavedState, LoadError> {
|
||||||
|
use async_std::prelude::*;
|
||||||
|
|
||||||
|
let mut contents = String::new();
|
||||||
|
|
||||||
|
let mut file = async_std::fs::File::open(Self::path())
|
||||||
|
.await
|
||||||
|
.map_err(|_| LoadError::File)?;
|
||||||
|
|
||||||
|
file.read_to_string(&mut contents)
|
||||||
|
.await
|
||||||
|
.map_err(|_| LoadError::File)?;
|
||||||
|
|
||||||
|
serde_json::from_str(&contents).map_err(|_| LoadError::Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save(self) -> Result<(), SaveError> {
|
||||||
|
use async_std::prelude::*;
|
||||||
|
|
||||||
|
let json = serde_json::to_string_pretty(&self)
|
||||||
|
.map_err(|_| SaveError::Format)?;
|
||||||
|
|
||||||
|
let path = Self::path();
|
||||||
|
|
||||||
|
if let Some(dir) = path.parent() {
|
||||||
|
async_std::fs::create_dir_all(dir)
|
||||||
|
.await
|
||||||
|
.map_err(|_| SaveError::File)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut file = async_std::fs::File::create(path)
|
||||||
|
.await
|
||||||
|
.map_err(|_| SaveError::File)?;
|
||||||
|
|
||||||
|
file.write_all(json.as_bytes())
|
||||||
|
.await
|
||||||
|
.map_err(|_| SaveError::Write)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a simple way to save at most once every couple seconds
|
||||||
|
async_std::task::sleep(std::time::Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
impl SavedState {
|
||||||
|
fn storage() -> Option<web_sys::Storage> {
|
||||||
|
let window = web_sys::window()?;
|
||||||
|
|
||||||
|
window.local_storage().ok()?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load() -> Result<SavedState, LoadError> {
|
||||||
|
let storage = Self::storage().ok_or(LoadError::File)?;
|
||||||
|
|
||||||
|
let contents = storage
|
||||||
|
.get_item("state")
|
||||||
|
.map_err(|_| LoadError::File)?
|
||||||
|
.ok_or(LoadError::File)?;
|
||||||
|
|
||||||
|
serde_json::from_str(&contents).map_err(|_| LoadError::Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save(self) -> Result<(), SaveError> {
|
||||||
|
let storage = Self::storage().ok_or(SaveError::File)?;
|
||||||
|
|
||||||
|
let json = serde_json::to_string_pretty(&self)
|
||||||
|
.map_err(|_| SaveError::Format)?;
|
||||||
|
|
||||||
|
storage
|
||||||
|
.set_item("state", &json)
|
||||||
|
.map_err(|_| SaveError::Write)?;
|
||||||
|
|
||||||
|
let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,4 +7,4 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
iced.features = ["svg"]
|
iced.features = ["svg", "winit", "tokio"]
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "system_information"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Richard <richardsoncusto@gmail.com>"]
|
|
||||||
edition = "2024"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
iced.workspace = true
|
|
||||||
iced.features = ["sysinfo"]
|
|
||||||
|
|
||||||
bytesize = "1.1"
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
use iced::system;
|
|
||||||
use iced::widget::{button, center, column, text};
|
|
||||||
use iced::{Element, Task};
|
|
||||||
|
|
||||||
pub fn main() -> iced::Result {
|
|
||||||
iced::application(Example::new, Example::update, Example::view).run()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
enum Example {
|
|
||||||
#[default]
|
|
||||||
Loading,
|
|
||||||
Loaded {
|
|
||||||
information: system::Information,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
enum Message {
|
|
||||||
InformationReceived(system::Information),
|
|
||||||
Refresh,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Example {
|
|
||||||
fn new() -> (Self, Task<Message>) {
|
|
||||||
(
|
|
||||||
Self::Loading,
|
|
||||||
system::information().map(Message::InformationReceived),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, message: Message) -> Task<Message> {
|
|
||||||
match message {
|
|
||||||
Message::Refresh => {
|
|
||||||
let (state, refresh) = Self::new();
|
|
||||||
|
|
||||||
*self = state;
|
|
||||||
|
|
||||||
refresh
|
|
||||||
}
|
|
||||||
Message::InformationReceived(information) => {
|
|
||||||
*self = Self::Loaded { information };
|
|
||||||
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&self) -> Element<'_, Message> {
|
|
||||||
use bytesize::ByteSize;
|
|
||||||
|
|
||||||
let content: Element<_> = match self {
|
|
||||||
Example::Loading => text("Loading...").size(40).into(),
|
|
||||||
Example::Loaded { information } => {
|
|
||||||
let system_name = text!(
|
|
||||||
"System name: {}",
|
|
||||||
information
|
|
||||||
.system_name
|
|
||||||
.as_ref()
|
|
||||||
.unwrap_or(&"unknown".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
let system_kernel = text!(
|
|
||||||
"System kernel: {}",
|
|
||||||
information
|
|
||||||
.system_kernel
|
|
||||||
.as_ref()
|
|
||||||
.unwrap_or(&"unknown".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
let system_version = text!(
|
|
||||||
"System version: {}",
|
|
||||||
information
|
|
||||||
.system_version
|
|
||||||
.as_ref()
|
|
||||||
.unwrap_or(&"unknown".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
let system_short_version = text!(
|
|
||||||
"System short version: {}",
|
|
||||||
information
|
|
||||||
.system_short_version
|
|
||||||
.as_ref()
|
|
||||||
.unwrap_or(&"unknown".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
let cpu_brand =
|
|
||||||
text!("Processor brand: {}", information.cpu_brand);
|
|
||||||
|
|
||||||
let cpu_cores = text!(
|
|
||||||
"Processor cores: {}",
|
|
||||||
information
|
|
||||||
.cpu_cores
|
|
||||||
.map_or("unknown".to_string(), |cores| cores
|
|
||||||
.to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
let memory_readable =
|
|
||||||
ByteSize::b(information.memory_total).to_string_as(true);
|
|
||||||
|
|
||||||
let memory_total = text!(
|
|
||||||
"Memory (total): {} bytes ({memory_readable})",
|
|
||||||
information.memory_total,
|
|
||||||
);
|
|
||||||
|
|
||||||
let memory_text =
|
|
||||||
if let Some(memory_used) = information.memory_used {
|
|
||||||
let memory_readable =
|
|
||||||
ByteSize::b(memory_used).to_string_as(true);
|
|
||||||
|
|
||||||
format!("{memory_used} bytes ({memory_readable})")
|
|
||||||
} else {
|
|
||||||
String::from("None")
|
|
||||||
};
|
|
||||||
|
|
||||||
let memory_used = text!("Memory (used): {memory_text}");
|
|
||||||
|
|
||||||
let graphics_adapter =
|
|
||||||
text!("Graphics adapter: {}", information.graphics_adapter);
|
|
||||||
|
|
||||||
let graphics_backend =
|
|
||||||
text!("Graphics backend: {}", information.graphics_backend);
|
|
||||||
|
|
||||||
column![
|
|
||||||
system_name.size(30),
|
|
||||||
system_kernel.size(30),
|
|
||||||
system_version.size(30),
|
|
||||||
system_short_version.size(30),
|
|
||||||
cpu_brand.size(30),
|
|
||||||
cpu_cores.size(30),
|
|
||||||
memory_total.size(30),
|
|
||||||
memory_used.size(30),
|
|
||||||
graphics_adapter.size(30),
|
|
||||||
graphics_backend.size(30),
|
|
||||||
button("Refresh").on_press(Message::Refresh)
|
|
||||||
]
|
|
||||||
.spacing(10)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
center(content).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -166,7 +166,9 @@ mod toast {
|
||||||
use iced::advanced::layout::{self, Layout};
|
use iced::advanced::layout::{self, Layout};
|
||||||
use iced::advanced::overlay;
|
use iced::advanced::overlay;
|
||||||
use iced::advanced::renderer;
|
use iced::advanced::renderer;
|
||||||
use iced::advanced::widget::{self, Operation, Tree};
|
use iced::advanced::widget::{
|
||||||
|
self, Operation, OperationOutputWrapper, Tree,
|
||||||
|
};
|
||||||
use iced::advanced::{Clipboard, Shell, Widget};
|
use iced::advanced::{Clipboard, Shell, Widget};
|
||||||
use iced::mouse;
|
use iced::mouse;
|
||||||
use iced::time::{self, Duration, Instant};
|
use iced::time::{self, Duration, Instant};
|
||||||
|
|
@ -319,7 +321,7 @@ mod toast {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff(&self, tree: &mut Tree) {
|
fn diff(&mut self, tree: &mut Tree) {
|
||||||
let instants = tree.state.downcast_mut::<Vec<Option<Instant>>>();
|
let instants = tree.state.downcast_mut::<Vec<Option<Instant>>>();
|
||||||
|
|
||||||
// Invalidating removed instants to None allows us to remove
|
// Invalidating removed instants to None allows us to remove
|
||||||
|
|
@ -341,8 +343,8 @@ mod toast {
|
||||||
}
|
}
|
||||||
|
|
||||||
tree.diff_children(
|
tree.diff_children(
|
||||||
&std::iter::once(&self.content)
|
&mut std::iter::once(&mut self.content)
|
||||||
.chain(self.toasts.iter())
|
.chain(self.toasts.iter_mut())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ tester = ["iced/tester"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
iced.features = ["tokio", "debug", "time-travel"]
|
iced.features = ["tokio", "debug", "time-travel", "winit"]
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
@ -35,6 +35,14 @@ iced_test.workspace = true
|
||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
assets = [
|
assets = [
|
||||||
["target/release-opt/todos", "usr/bin/iced-todos", "755"],
|
[
|
||||||
["iced-todos.desktop", "usr/share/applications/", "644"],
|
"target/release-opt/todos",
|
||||||
|
"usr/bin/iced-todos",
|
||||||
|
"755",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"iced-todos.desktop",
|
||||||
|
"usr/share/applications/",
|
||||||
|
"644",
|
||||||
|
],
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ use iced::widget::{
|
||||||
};
|
};
|
||||||
use iced::window;
|
use iced::window;
|
||||||
use iced::{
|
use iced::{
|
||||||
Application, Center, Element, Fill, Font, Function, Preset, Program,
|
Center, Element, Fill, Font, Function, Preset, Program, Subscription,
|
||||||
Subscription, Task as Command, Theme,
|
Task as Command, Theme, application::Application,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,4 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
iced.features = ["debug"]
|
iced.features = ["debug", "winit", "tiny-skia"]
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
iced.features = ["image", "debug"]
|
iced.features = ["image", "debug", "winit"]
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pub fn main() -> iced::Result {
|
||||||
.subscription(WebSocket::subscription)
|
.subscription(WebSocket::subscription)
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
use iced::id::Id;
|
||||||
|
|
||||||
struct WebSocket {
|
struct WebSocket {
|
||||||
messages: Vec<echo::Message>,
|
messages: Vec<echo::Message>,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ all-features = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
thread-pool = ["futures/thread-pool"]
|
thread-pool = ["futures/thread-pool"]
|
||||||
|
a11y = ["iced_core/a11y"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced_core.workspace = true
|
iced_core.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ where
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
let future =
|
let future =
|
||||||
stream.map(Ok).forward(sender).map(|result| match result {
|
stream.map(Ok).forward(sender).map(|result| match result {
|
||||||
Ok(()) => (),
|
Ok(()) => {}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Stream could not run until completion: {error}"
|
"Stream could not run until completion: {error}"
|
||||||
|
|
|
||||||
|
|
@ -116,10 +116,9 @@ impl<T> Window for T where
|
||||||
///
|
///
|
||||||
/// This is just a convenient super trait of the `raw-window-handle`
|
/// This is just a convenient super trait of the `raw-window-handle`
|
||||||
/// trait.
|
/// trait.
|
||||||
pub trait Display: HasDisplayHandle + MaybeSend + MaybeSync + 'static {}
|
pub trait Display: HasDisplayHandle + 'static {}
|
||||||
|
|
||||||
impl<T> Display for T where T: HasDisplayHandle + MaybeSend + MaybeSync + 'static
|
impl<T> Display for T where T: HasDisplayHandle + 'static {}
|
||||||
{}
|
|
||||||
|
|
||||||
/// Defines the default compositor of a renderer.
|
/// Defines the default compositor of a renderer.
|
||||||
pub trait Default {
|
pub trait Default {
|
||||||
|
|
@ -147,6 +146,18 @@ pub enum SurfaceError {
|
||||||
/// Acquiring a texture failed with a generic error.
|
/// Acquiring a texture failed with a generic error.
|
||||||
#[error("Acquiring a texture failed with a generic error")]
|
#[error("Acquiring a texture failed with a generic error")]
|
||||||
Other,
|
Other,
|
||||||
|
/// Resize Error
|
||||||
|
#[error("Resize Error")]
|
||||||
|
Resize,
|
||||||
|
/// Invalid dimensions
|
||||||
|
#[error("Invalid dimensions")]
|
||||||
|
InvalidDimensions,
|
||||||
|
/// Present Error
|
||||||
|
#[error("Present Error")]
|
||||||
|
Present(String),
|
||||||
|
/// Present Error
|
||||||
|
#[error("No damage to present")]
|
||||||
|
NoDamage,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contains information about the graphics (e.g. graphics adapter, graphics backend).
|
/// Contains information about the graphics (e.g. graphics adapter, graphics backend).
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,41 @@ impl Text {
|
||||||
wrapping: Wrapping::default(),
|
wrapping: Wrapping::default(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut buffer = cosmic_text::BufferLine::new(
|
||||||
|
&self.content,
|
||||||
|
cosmic_text::LineEnding::default(),
|
||||||
|
cosmic_text::AttrsList::new(&text::to_attributes(self.font)),
|
||||||
|
text::to_shaping(self.shaping, &self.content),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut font_system = text::font_system().write().unwrap();
|
||||||
|
let layout = buffer.layout(
|
||||||
|
font_system.raw(),
|
||||||
|
self.size.0,
|
||||||
|
None,
|
||||||
|
cosmic_text::Wrap::None,
|
||||||
|
None,
|
||||||
|
8,
|
||||||
|
cosmic_text::Hinting::Disabled,
|
||||||
|
);
|
||||||
|
|
||||||
let translation_x = match self.align_x {
|
let translation_x = match self.align_x {
|
||||||
Alignment::Default | Alignment::Left | Alignment::Justified => {
|
Alignment::Left | Alignment::Default | Alignment::Justified => {
|
||||||
self.position.x
|
self.position.x
|
||||||
}
|
}
|
||||||
Alignment::Center => self.position.x - paragraph.min_width() / 2.0,
|
Alignment::Center | Alignment::Right => {
|
||||||
Alignment::Right => self.position.x - paragraph.min_width(),
|
let mut line_width = 0.0f32;
|
||||||
|
|
||||||
|
for line in layout.iter() {
|
||||||
|
line_width = line_width.max(line.w);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.align_x == Alignment::Center {
|
||||||
|
self.position.x - line_width / 2.0
|
||||||
|
} else {
|
||||||
|
self.position.x - line_width
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let translation_y = {
|
let translation_y = {
|
||||||
|
|
@ -171,12 +200,12 @@ impl Default for Text {
|
||||||
position: Point::ORIGIN,
|
position: Point::ORIGIN,
|
||||||
max_width: f32::INFINITY,
|
max_width: f32::INFINITY,
|
||||||
color: Color::BLACK,
|
color: Color::BLACK,
|
||||||
size: Pixels(16.0),
|
size: Pixels(14.0),
|
||||||
line_height: LineHeight::Relative(1.2),
|
line_height: LineHeight::default(),
|
||||||
font: Font::default(),
|
font: Font::default(),
|
||||||
align_x: Alignment::Default,
|
align_x: Alignment::Default,
|
||||||
align_y: alignment::Vertical::Top,
|
align_y: alignment::Vertical::Top,
|
||||||
shaping: Shaping::default(),
|
shaping: Shaping::Advanced,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
#[cfg(feature = "image")]
|
#[cfg(feature = "image")]
|
||||||
use crate::core::Bytes;
|
use crate::core::Bytes;
|
||||||
|
|
||||||
|
use crate::core::Color;
|
||||||
|
use crate::core::Radians;
|
||||||
use crate::core::Rectangle;
|
use crate::core::Rectangle;
|
||||||
use crate::core::image;
|
use crate::core::image;
|
||||||
use crate::core::svg;
|
use crate::core::svg;
|
||||||
|
|
@ -117,7 +119,14 @@ pub fn load(handle: &image::Handle) -> Result<Buffer, image::Error> {
|
||||||
|
|
||||||
let (width, height, pixels) = match handle {
|
let (width, height, pixels) = match handle {
|
||||||
image::Handle::Path(_, path) => {
|
image::Handle::Path(_, path) => {
|
||||||
let image = ::image::open(path).map_err(to_error)?;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
let image = ::image::ImageReader::open(&path)
|
||||||
|
.map_err(|e| image::Error::Inaccessible(Arc::new(e)))?
|
||||||
|
.with_guessed_format()
|
||||||
|
.map_err(|e| image::Error::Invalid(Arc::new(e)))?
|
||||||
|
.decode()
|
||||||
|
.map_err(|e| image::Error::Invalid(Arc::new(e)))?;
|
||||||
|
|
||||||
let operation = std::fs::File::open(path)
|
let operation = std::fs::File::open(path)
|
||||||
.ok()
|
.ok()
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ pub struct Settings {
|
||||||
|
|
||||||
/// The default size of text.
|
/// The default size of text.
|
||||||
///
|
///
|
||||||
/// By default, it will be set to `16.0`.
|
/// By default, it will be set to `14.0`.
|
||||||
pub default_text_size: Pixels,
|
pub default_text_size: Pixels,
|
||||||
|
|
||||||
/// The antialiasing strategy that will be used for triangle primitives.
|
/// The antialiasing strategy that will be used for triangle primitives.
|
||||||
|
|
@ -27,7 +27,7 @@ impl Default for Settings {
|
||||||
fn default() -> Settings {
|
fn default() -> Settings {
|
||||||
Settings {
|
Settings {
|
||||||
default_font: Font::default(),
|
default_font: Font::default(),
|
||||||
default_text_size: Pixels(16.0),
|
default_text_size: Pixels(14.0),
|
||||||
antialiasing: None,
|
antialiasing: None,
|
||||||
vsync: true,
|
vsync: true,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue