mirror of https://github.com/linebender/xilem
Integrate `tokio` for async communication with Xilem (#423)
Supercedes https://github.com/linebender/xilem/pull/411 This is designed with #417 in mind, to not lock-in to our event loop. --------- Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me>
This commit is contained in:
parent
732cfa8376
commit
7f40266bd8
|
@ -8,7 +8,7 @@ env:
|
||||||
# If the compilation fails, then the version specified here needs to be bumped up to reality.
|
# If the compilation fails, then the version specified here needs to be bumped up to reality.
|
||||||
# Be sure to also update the rust-version property in the workspace Cargo.toml file,
|
# Be sure to also update the rust-version property in the workspace Cargo.toml file,
|
||||||
# plus all the README.md files of the affected packages.
|
# plus all the README.md files of the affected packages.
|
||||||
RUST_MIN_VER: "1.77"
|
RUST_MIN_VER: "1.79"
|
||||||
# List of packages that will be checked with the minimum supported Rust version.
|
# List of packages that will be checked with the minimum supported Rust version.
|
||||||
# This should be limited to packages that are intended for publishing.
|
# This should be limited to packages that are intended for publishing.
|
||||||
# If updating, synchronise RUST_MIN_VER_WASM_PKGS
|
# If updating, synchronise RUST_MIN_VER_WASM_PKGS
|
||||||
|
|
|
@ -107,6 +107,15 @@ dependencies = [
|
||||||
"winit",
|
"winit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "addr2line"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
|
||||||
|
dependencies = [
|
||||||
|
"gimli",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler"
|
name = "adler"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
@ -440,6 +449,21 @@ version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backtrace"
|
||||||
|
version = "0.3.73"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
|
||||||
|
dependencies = [
|
||||||
|
"addr2line",
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"miniz_oxide",
|
||||||
|
"object",
|
||||||
|
"rustc-demangle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
@ -1268,6 +1292,12 @@ dependencies = [
|
||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gimli"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gl_generator"
|
name = "gl_generator"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
@ -1940,6 +1970,16 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_cpus"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_enum"
|
name = "num_enum"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
|
@ -2173,6 +2213,15 @@ dependencies = [
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "object"
|
||||||
|
version = "0.36.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
|
@ -2571,6 +2620,12 @@ version = "0.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-demangle"
|
||||||
|
version = "0.1.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -3023,6 +3078,17 @@ dependencies = [
|
||||||
"xilem_web",
|
"xilem_web",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.38.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"num_cpus",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.6.5"
|
version = "0.6.5"
|
||||||
|
@ -4005,6 +4071,7 @@ dependencies = [
|
||||||
"accesskit_winit",
|
"accesskit_winit",
|
||||||
"masonry",
|
"masonry",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"vello",
|
"vello",
|
||||||
"winit",
|
"winit",
|
||||||
|
|
|
@ -17,7 +17,7 @@ members = [
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
# Keep in sync with RUST_MIN_VER in .github/workflows/ci.yml, with the relevant README.md files.
|
# Keep in sync with RUST_MIN_VER in .github/workflows/ci.yml, with the relevant README.md files.
|
||||||
rust-version = "1.77"
|
rust-version = "1.79"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
repository = "https://github.com/linebender/xilem"
|
repository = "https://github.com/linebender/xilem"
|
||||||
homepage = "https://xilem.dev/"
|
homepage = "https://xilem.dev/"
|
||||||
|
|
|
@ -90,7 +90,7 @@ fn main() {
|
||||||
|
|
||||||
## Minimum supported Rust Version (MSRV)
|
## Minimum supported Rust Version (MSRV)
|
||||||
|
|
||||||
This version of Masonry has been verified to compile with **Rust 1.77** and later.
|
This version of Masonry has been verified to compile with **Rust 1.79** and later.
|
||||||
|
|
||||||
Future versions of Masonry might increase the Rust version requirement.
|
Future versions of Masonry might increase the Rust version requirement.
|
||||||
It will not be treated as a breaking change and as such can even happen with small patch releases.
|
It will not be treated as a breaking change and as such can even happen with small patch releases.
|
||||||
|
|
|
@ -7,8 +7,6 @@
|
||||||
#![windows_subsystem = "windows"]
|
#![windows_subsystem = "windows"]
|
||||||
#![allow(clippy::single_match)]
|
#![allow(clippy::single_match)]
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use accesskit::{DefaultActionVerb, Role};
|
use accesskit::{DefaultActionVerb, Role};
|
||||||
use masonry::app_driver::{AppDriver, DriverCtx};
|
use masonry::app_driver::{AppDriver, DriverCtx};
|
||||||
use masonry::dpi::LogicalSize;
|
use masonry::dpi::LogicalSize;
|
||||||
|
@ -156,7 +154,7 @@ impl Widget for CalcButton {
|
||||||
}
|
}
|
||||||
PointerEvent::PointerUp(_, _) => {
|
PointerEvent::PointerUp(_, _) => {
|
||||||
if ctx.is_active() && !ctx.is_disabled() {
|
if ctx.is_active() && !ctx.is_disabled() {
|
||||||
ctx.submit_action(Action::Other(Arc::new(self.action)));
|
ctx.submit_action(Action::Other(Box::new(self.action)));
|
||||||
ctx.request_paint();
|
ctx.request_paint();
|
||||||
trace!("CalcButton {:?} released", ctx.widget_id());
|
trace!("CalcButton {:?} released", ctx.widget_id());
|
||||||
}
|
}
|
||||||
|
@ -176,7 +174,7 @@ impl Widget for CalcButton {
|
||||||
if event.target == ctx.widget_id() {
|
if event.target == ctx.widget_id() {
|
||||||
match event.action {
|
match event.action {
|
||||||
accesskit::Action::Default => {
|
accesskit::Action::Default => {
|
||||||
ctx.submit_action(Action::Other(Arc::new(self.action)));
|
ctx.submit_action(Action::Other(Box::new(self.action)));
|
||||||
ctx.request_paint();
|
ctx.request_paint();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::event::PointerButton;
|
use crate::event::PointerButton;
|
||||||
|
|
||||||
|
@ -20,7 +19,7 @@ pub enum Action {
|
||||||
TextEntered(String),
|
TextEntered(String),
|
||||||
CheckboxChecked(bool),
|
CheckboxChecked(bool),
|
||||||
// FIXME - This is a huge hack
|
// FIXME - This is a huge hack
|
||||||
Other(Arc<dyn Any + Send + Sync>),
|
Other(Box<dyn Any + Send>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Action {
|
impl PartialEq for Action {
|
||||||
|
@ -30,9 +29,8 @@ impl PartialEq for Action {
|
||||||
(Self::TextChanged(l0), Self::TextChanged(r0)) => l0 == r0,
|
(Self::TextChanged(l0), Self::TextChanged(r0)) => l0 == r0,
|
||||||
(Self::TextEntered(l0), Self::TextEntered(r0)) => l0 == r0,
|
(Self::TextEntered(l0), Self::TextEntered(r0)) => l0 == r0,
|
||||||
(Self::CheckboxChecked(l0), Self::CheckboxChecked(r0)) => l0 == r0,
|
(Self::CheckboxChecked(l0), Self::CheckboxChecked(r0)) => l0 == r0,
|
||||||
#[allow(ambiguous_wide_pointer_comparisons)]
|
|
||||||
// FIXME
|
// FIXME
|
||||||
(Self::Other(val_l), Self::Other(val_r)) => Arc::ptr_eq(val_l, val_r),
|
// (Self::Other(val_l), Self::Other(val_r)) => false,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,14 +16,27 @@ use winit::event::{
|
||||||
DeviceEvent as WinitDeviceEvent, DeviceId, MouseButton as WinitMouseButton,
|
DeviceEvent as WinitDeviceEvent, DeviceId, MouseButton as WinitMouseButton,
|
||||||
WindowEvent as WinitWindowEvent,
|
WindowEvent as WinitWindowEvent,
|
||||||
};
|
};
|
||||||
use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
|
use winit::event_loop::ActiveEventLoop;
|
||||||
use winit::window::{Window, WindowAttributes, WindowId};
|
use winit::window::{Window, WindowAttributes, WindowId};
|
||||||
|
|
||||||
use crate::app_driver::{AppDriver, DriverCtx};
|
use crate::app_driver::{AppDriver, DriverCtx};
|
||||||
use crate::dpi::LogicalPosition;
|
use crate::dpi::LogicalPosition;
|
||||||
use crate::event::{PointerButton, PointerState, WindowEvent};
|
use crate::event::{PointerButton, PointerState, WindowEvent};
|
||||||
use crate::render_root::{self, RenderRoot, WindowSizePolicy};
|
use crate::render_root::{self, RenderRoot, WindowSizePolicy};
|
||||||
use crate::{PointerEvent, TextEvent, Widget};
|
use crate::{PointerEvent, TextEvent, Widget, WidgetId};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MasonryUserEvent {
|
||||||
|
AccessKit(accesskit_winit::Event),
|
||||||
|
// TODO: A more considered design here
|
||||||
|
Action(crate::Action, WidgetId),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<accesskit_winit::Event> for MasonryUserEvent {
|
||||||
|
fn from(value: accesskit_winit::Event) -> Self {
|
||||||
|
Self::AccessKit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<WinitMouseButton> for PointerButton {
|
impl From<WinitMouseButton> for PointerButton {
|
||||||
fn from(button: WinitMouseButton) -> Self {
|
fn from(button: WinitMouseButton) -> Self {
|
||||||
|
@ -64,7 +77,7 @@ pub struct MasonryState<'a> {
|
||||||
renderer: Option<Renderer>,
|
renderer: Option<Renderer>,
|
||||||
// TODO: Winit doesn't seem to let us create these proxies from within the loop
|
// TODO: Winit doesn't seem to let us create these proxies from within the loop
|
||||||
// The reasons for this are unclear
|
// The reasons for this are unclear
|
||||||
proxy: EventLoopProxy<accesskit_winit::Event>,
|
proxy: EventLoopProxy,
|
||||||
|
|
||||||
// Per-Window state
|
// Per-Window state
|
||||||
// In future, this will support multiple windows
|
// In future, this will support multiple windows
|
||||||
|
@ -79,11 +92,14 @@ struct MainState<'a> {
|
||||||
/// The type of the event loop used by Masonry.
|
/// The type of the event loop used by Masonry.
|
||||||
///
|
///
|
||||||
/// This *will* be changed to allow custom event types, but is implemented this way for expedience
|
/// This *will* be changed to allow custom event types, but is implemented this way for expedience
|
||||||
pub type EventLoop = winit::event_loop::EventLoop<accesskit_winit::Event>;
|
pub type EventLoop = winit::event_loop::EventLoop<MasonryUserEvent>;
|
||||||
/// The type of the event loop builder used by Masonry.
|
/// The type of the event loop builder used by Masonry.
|
||||||
///
|
///
|
||||||
/// This *will* be changed to allow custom event types, but is implemented this way for expedience
|
/// This *will* be changed to allow custom event types, but is implemented this way for expedience
|
||||||
pub type EventLoopBuilder = winit::event_loop::EventLoopBuilder<accesskit_winit::Event>;
|
pub type EventLoopBuilder = winit::event_loop::EventLoopBuilder<MasonryUserEvent>;
|
||||||
|
|
||||||
|
/// A proxy used to send events to the event loop
|
||||||
|
pub type EventLoopProxy = winit::event_loop::EventLoopProxy<MasonryUserEvent>;
|
||||||
|
|
||||||
// --- MARK: RUN ---
|
// --- MARK: RUN ---
|
||||||
pub fn run(
|
pub fn run(
|
||||||
|
@ -97,12 +113,12 @@ pub fn run(
|
||||||
) -> Result<(), EventLoopError> {
|
) -> Result<(), EventLoopError> {
|
||||||
let event_loop = loop_builder.build()?;
|
let event_loop = loop_builder.build()?;
|
||||||
|
|
||||||
run_with(window_attributes, event_loop, root_widget, app_driver)
|
run_with(event_loop, window_attributes, root_widget, app_driver)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_with(
|
pub fn run_with(
|
||||||
window: WindowAttributes,
|
|
||||||
event_loop: EventLoop,
|
event_loop: EventLoop,
|
||||||
|
window: WindowAttributes,
|
||||||
root_widget: impl Widget,
|
root_widget: impl Widget,
|
||||||
app_driver: impl AppDriver + 'static,
|
app_driver: impl AppDriver + 'static,
|
||||||
) -> Result<(), EventLoopError> {
|
) -> Result<(), EventLoopError> {
|
||||||
|
@ -120,7 +136,7 @@ pub fn run_with(
|
||||||
event_loop.run_app(&mut main_state)
|
event_loop.run_app(&mut main_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApplicationHandler<accesskit_winit::Event> for MainState<'_> {
|
impl ApplicationHandler<MasonryUserEvent> for MainState<'_> {
|
||||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
self.masonry_state.handle_resumed(event_loop);
|
self.masonry_state.handle_resumed(event_loop);
|
||||||
}
|
}
|
||||||
|
@ -157,7 +173,7 @@ impl ApplicationHandler<accesskit_winit::Event> for MainState<'_> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: accesskit_winit::Event) {
|
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: MasonryUserEvent) {
|
||||||
self.masonry_state
|
self.masonry_state
|
||||||
.handle_user_event(event_loop, event, self.app_driver.as_mut());
|
.handle_user_event(event_loop, event, self.app_driver.as_mut());
|
||||||
}
|
}
|
||||||
|
@ -526,20 +542,29 @@ impl MasonryState<'_> {
|
||||||
pub fn handle_user_event(
|
pub fn handle_user_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event_loop: &ActiveEventLoop,
|
event_loop: &ActiveEventLoop,
|
||||||
event: accesskit_winit::Event,
|
event: MasonryUserEvent,
|
||||||
app_driver: &mut dyn AppDriver,
|
app_driver: &mut dyn AppDriver,
|
||||||
) {
|
) {
|
||||||
match event.window_event {
|
match event {
|
||||||
// Note that this event can be called at any time, even multiple times if
|
MasonryUserEvent::AccessKit(event) => {
|
||||||
// the user restarts their screen reader.
|
match event.window_event {
|
||||||
accesskit_winit::WindowEvent::InitialTreeRequested => {
|
// Note that this event can be called at any time, even multiple times if
|
||||||
self.render_root
|
// the user restarts their screen reader.
|
||||||
.handle_window_event(WindowEvent::RebuildAccessTree);
|
accesskit_winit::WindowEvent::InitialTreeRequested => {
|
||||||
|
self.render_root
|
||||||
|
.handle_window_event(WindowEvent::RebuildAccessTree);
|
||||||
|
}
|
||||||
|
accesskit_winit::WindowEvent::ActionRequested(action_request) => {
|
||||||
|
self.render_root.root_on_access_event(action_request);
|
||||||
|
}
|
||||||
|
accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
accesskit_winit::WindowEvent::ActionRequested(action_request) => {
|
MasonryUserEvent::Action(action, widget) => self
|
||||||
self.render_root.root_on_access_event(action_request);
|
.render_root
|
||||||
}
|
.state
|
||||||
accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
|
.signal_queue
|
||||||
|
.push_back(render_root::RenderRootSignal::Action(action, widget)),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.handle_signals(event_loop, app_driver);
|
self.handle_signals(event_loop, app_driver);
|
||||||
|
|
|
@ -107,6 +107,7 @@ impl WidgetMut<'_, Label> {
|
||||||
let ret = f(&mut self.widget.text_layout);
|
let ret = f(&mut self.widget.text_layout);
|
||||||
if self.widget.text_layout.needs_rebuild() {
|
if self.widget.text_layout.needs_rebuild() {
|
||||||
self.ctx.request_layout();
|
self.ctx.request_layout();
|
||||||
|
self.ctx.request_paint();
|
||||||
}
|
}
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
@ -151,7 +152,7 @@ impl Widget for Label {
|
||||||
// TODO: Set cursor if over link
|
// TODO: Set cursor if over link
|
||||||
}
|
}
|
||||||
PointerEvent::PointerDown(_button, _state) => {
|
PointerEvent::PointerDown(_button, _state) => {
|
||||||
// TODO: Start tracking currently pressed link
|
// TODO: Start tracking currently pressed
|
||||||
// (i.e. don't press)
|
// (i.e. don't press)
|
||||||
}
|
}
|
||||||
PointerEvent::PointerUp(_button, _state) => {
|
PointerEvent::PointerUp(_button, _state) => {
|
||||||
|
|
|
@ -36,6 +36,7 @@ vello.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
accesskit.workspace = true
|
accesskit.workspace = true
|
||||||
accesskit_winit.workspace = true
|
accesskit_winit.workspace = true
|
||||||
|
tokio = { version = "1.38.0", features = ["rt", "rt-multi-thread", "time"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dev-dependencies]
|
[target.'cfg(target_os = "android")'.dev-dependencies]
|
||||||
winit = { features = ["android-native-activity"], workspace = true }
|
winit = { features = ["android-native-activity"], workspace = true }
|
||||||
|
|
|
@ -33,7 +33,7 @@ Lots of things need improvements.
|
||||||
|
|
||||||
## Minimum supported Rust Version (MSRV)
|
## Minimum supported Rust Version (MSRV)
|
||||||
|
|
||||||
This version of Xilem has been verified to compile with **Rust 1.77** and later.
|
This version of Xilem has been verified to compile with **Rust 1.79** and later.
|
||||||
|
|
||||||
Future versions of Xilem might increase the Rust version requirement.
|
Future versions of Xilem might increase the Rust version requirement.
|
||||||
It will not be treated as a breaking change and as such can even happen with small patch releases.
|
It will not be treated as a breaking change and as such can even happen with small patch releases.
|
||||||
|
|
|
@ -5,8 +5,11 @@
|
||||||
//! Currently, this supports running as its own window alongside an existing application, or
|
//! Currently, this supports running as its own window alongside an existing application, or
|
||||||
//! accessing raw events from winit.
|
//! accessing raw events from winit.
|
||||||
//! Support for more custom embeddings would be welcome, but needs more design work
|
//! Support for more custom embeddings would be welcome, but needs more design work
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use masonry::{
|
use masonry::{
|
||||||
app_driver::AppDriver,
|
app_driver::AppDriver,
|
||||||
|
event_loop_runner::MasonryUserEvent,
|
||||||
widget::{CrossAxisAlignment, MainAxisAlignment},
|
widget::{CrossAxisAlignment, MainAxisAlignment},
|
||||||
ArcStr,
|
ArcStr,
|
||||||
};
|
};
|
||||||
|
@ -18,7 +21,7 @@ use winit::{
|
||||||
};
|
};
|
||||||
use xilem::{
|
use xilem::{
|
||||||
view::{button, flex, label, sized_box},
|
view::{button, flex, label, sized_box},
|
||||||
EventLoop, WidgetView, Xilem,
|
EventLoop, MasonryProxy, WidgetView, Xilem,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A component to make a bigger than usual button
|
/// A component to make a bigger than usual button
|
||||||
|
@ -50,7 +53,7 @@ struct ExternalApp {
|
||||||
app_driver: Box<dyn AppDriver>,
|
app_driver: Box<dyn AppDriver>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApplicationHandler<accesskit_winit::Event> for ExternalApp {
|
impl ApplicationHandler<MasonryUserEvent> for ExternalApp {
|
||||||
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
|
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
|
||||||
self.masonry_state.handle_resumed(event_loop);
|
self.masonry_state.handle_resumed(event_loop);
|
||||||
}
|
}
|
||||||
|
@ -80,7 +83,7 @@ impl ApplicationHandler<accesskit_winit::Event> for ExternalApp {
|
||||||
fn user_event(
|
fn user_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event_loop: &winit::event_loop::ActiveEventLoop,
|
event_loop: &winit::event_loop::ActiveEventLoop,
|
||||||
event: accesskit_winit::Event,
|
event: MasonryUserEvent,
|
||||||
) {
|
) {
|
||||||
self.masonry_state
|
self.masonry_state
|
||||||
.handle_user_event(event_loop, event, self.app_driver.as_mut());
|
.handle_user_event(event_loop, event, self.app_driver.as_mut());
|
||||||
|
@ -137,15 +140,14 @@ fn main() -> Result<(), EventLoopError> {
|
||||||
let xilem = Xilem::new(0, app_logic);
|
let xilem = Xilem::new(0, app_logic);
|
||||||
|
|
||||||
let event_loop = EventLoop::with_user_event().build().unwrap();
|
let event_loop = EventLoop::with_user_event().build().unwrap();
|
||||||
let masonry_state = masonry::event_loop_runner::MasonryState::new(
|
let proxy = MasonryProxy::new(event_loop.create_proxy());
|
||||||
window_attributes,
|
let (widget, driver) = xilem.into_driver(Arc::new(proxy));
|
||||||
&event_loop,
|
let masonry_state =
|
||||||
xilem.root_widget,
|
masonry::event_loop_runner::MasonryState::new(window_attributes, &event_loop, widget);
|
||||||
);
|
|
||||||
|
|
||||||
let mut app = ExternalApp {
|
let mut app = ExternalApp {
|
||||||
masonry_state,
|
masonry_state,
|
||||||
app_driver: Box::new(xilem.driver),
|
app_driver: Box::new(driver),
|
||||||
};
|
};
|
||||||
event_loop.run_app(&mut app)
|
event_loop.run_app(&mut app)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,11 @@
|
||||||
// On Windows platform, don't show a console when opening the app.
|
// On Windows platform, don't show a console when opening the app.
|
||||||
#![windows_subsystem = "windows"]
|
#![windows_subsystem = "windows"]
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use xilem::{
|
use xilem::{
|
||||||
view::{button, button_any_pointer, checkbox, flex, label, prose, textbox},
|
tokio::time,
|
||||||
|
view::{async_repeat, button, button_any_pointer, checkbox, flex, label, prose, textbox},
|
||||||
AnyWidgetView, Axis, Color, EventLoop, EventLoopBuilder, TextAlignment, WidgetView, Xilem,
|
AnyWidgetView, Axis, Color, EventLoop, EventLoopBuilder, TextAlignment, WidgetView, Xilem,
|
||||||
};
|
};
|
||||||
const LOREM: &str = r"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi cursus mi sed euismod euismod. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam placerat efficitur tellus at semper. Morbi ac risus magna. Donec ut cursus ex. Etiam quis posuere tellus. Mauris posuere dui et turpis mollis, vitae luctus tellus consectetur. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eu facilisis nisl.
|
const LOREM: &str = r"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi cursus mi sed euismod euismod. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam placerat efficitur tellus at semper. Morbi ac risus magna. Donec ut cursus ex. Etiam quis posuere tellus. Mauris posuere dui et turpis mollis, vitae luctus tellus consectetur. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eu facilisis nisl.
|
||||||
|
@ -32,38 +35,52 @@ fn app_logic(data: &mut AppData) -> impl WidgetView<AppData> {
|
||||||
let sequence = (0..count)
|
let sequence = (0..count)
|
||||||
.map(|x| button(format!("+{x}"), move |data: &mut AppData| data.count += x))
|
.map(|x| button(format!("+{x}"), move |data: &mut AppData| data.count += x))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
flex((
|
fork(
|
||||||
flex((
|
flex((
|
||||||
label("Label")
|
flex((
|
||||||
.brush(Color::REBECCA_PURPLE)
|
label("Label")
|
||||||
.alignment(TextAlignment::Start),
|
.brush(Color::REBECCA_PURPLE)
|
||||||
// TODO masonry doesn't allow setting disabled manually anymore?
|
.alignment(TextAlignment::Start),
|
||||||
// label("Disabled label").disabled(),
|
// TODO masonry doesn't allow setting disabled manually anymore?
|
||||||
))
|
// label("Disabled label").disabled(),
|
||||||
.direction(Axis::Horizontal),
|
))
|
||||||
flex(textbox(
|
.direction(Axis::Horizontal),
|
||||||
data.textbox_contents.clone(),
|
flex(textbox(
|
||||||
|data: &mut AppData, new_value| {
|
data.textbox_contents.clone(),
|
||||||
data.textbox_contents = new_value;
|
|data: &mut AppData, new_value| {
|
||||||
|
data.textbox_contents = new_value;
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.direction(Axis::Horizontal),
|
||||||
|
prose(LOREM).alignment(TextAlignment::Middle).text_size(18.),
|
||||||
|
button_any_pointer(button_label, |data: &mut AppData, button| match button {
|
||||||
|
masonry::PointerButton::None => tracing::warn!("Got unexpected None from button"),
|
||||||
|
masonry::PointerButton::Primary => data.count += 1,
|
||||||
|
masonry::PointerButton::Secondary => data.count -= 1,
|
||||||
|
masonry::PointerButton::Auxiliary => data.count *= 2,
|
||||||
|
_ => (),
|
||||||
|
}),
|
||||||
|
checkbox("Check me", data.active, |data: &mut AppData, checked| {
|
||||||
|
data.active = checked;
|
||||||
|
}),
|
||||||
|
toggleable(data),
|
||||||
|
button("Decrement", |data: &mut AppData| data.count -= 1),
|
||||||
|
button("Reset", |data: &mut AppData| data.count = 0),
|
||||||
|
flex(sequence).direction(axis),
|
||||||
|
)),
|
||||||
|
async_repeat(
|
||||||
|
|proxy| async move {
|
||||||
|
let mut interval = time::interval(Duration::from_secs(1));
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
let Ok(()) = proxy.message(()) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
))
|
|data: &mut AppData, ()| data.count += 1,
|
||||||
.direction(Axis::Horizontal),
|
),
|
||||||
prose(LOREM).alignment(TextAlignment::Middle).text_size(18.),
|
)
|
||||||
button_any_pointer(button_label, |data: &mut AppData, button| match button {
|
|
||||||
masonry::PointerButton::None => tracing::warn!("Got unexpected None from button"),
|
|
||||||
masonry::PointerButton::Primary => data.count += 1,
|
|
||||||
masonry::PointerButton::Secondary => data.count -= 1,
|
|
||||||
masonry::PointerButton::Auxiliary => data.count *= 2,
|
|
||||||
_ => (),
|
|
||||||
}),
|
|
||||||
checkbox("Check me", data.active, |data: &mut AppData, checked| {
|
|
||||||
data.active = checked;
|
|
||||||
}),
|
|
||||||
toggleable(data),
|
|
||||||
button("Decrement", |data: &mut AppData| data.count -= 1),
|
|
||||||
button("Reset", |data: &mut AppData| data.count = 0),
|
|
||||||
flex(sequence).direction(axis),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggleable(data: &mut AppData) -> impl WidgetView<AppData> {
|
fn toggleable(data: &mut AppData) -> impl WidgetView<AppData> {
|
||||||
|
@ -76,6 +93,7 @@ fn toggleable(data: &mut AppData) -> impl WidgetView<AppData> {
|
||||||
button("Unlimited Power", |data: &mut AppData| {
|
button("Unlimited Power", |data: &mut AppData| {
|
||||||
data.count = -1_000_000;
|
data.count = -1_000_000;
|
||||||
}),
|
}),
|
||||||
|
run_once(|| tracing::warn!("The pathway to unlimited power has been revealed")),
|
||||||
))
|
))
|
||||||
.direction(Axis::Horizontal),
|
.direction(Axis::Horizontal),
|
||||||
)
|
)
|
||||||
|
@ -116,6 +134,7 @@ fn main() {
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
use winit::platform::android::activity::AndroidApp;
|
use winit::platform::android::activity::AndroidApp;
|
||||||
|
use xilem_core::{fork, run_once};
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
// Safety: We are following `android_activity`'s docs here
|
// Safety: We are following `android_activity`'s docs here
|
||||||
|
|
|
@ -56,6 +56,7 @@ impl<W: Widget> AnyElement<Pod<W>> for Pod<DynWidget> {
|
||||||
/// A widget whose only child can be dynamically replaced.
|
/// A widget whose only child can be dynamically replaced.
|
||||||
///
|
///
|
||||||
/// `WidgetPod<Box<dyn Widget>>` doesn't expose this possibility.
|
/// `WidgetPod<Box<dyn Widget>>` doesn't expose this possibility.
|
||||||
|
#[allow(unnameable_types)] // This is an implementation detail of `AnyWidgetView`
|
||||||
pub struct DynWidget {
|
pub struct DynWidget {
|
||||||
inner: WidgetPod<Box<dyn Widget>>,
|
inner: WidgetPod<Box<dyn Widget>>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
// Copyright 2024 the Xilem Authors
|
// Copyright 2024 the Xilem Authors
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use masonry::{app_driver::AppDriver, widget::RootWidget};
|
use std::sync::Arc;
|
||||||
use xilem_core::MessageResult;
|
|
||||||
|
use masonry::{
|
||||||
|
app_driver::AppDriver,
|
||||||
|
event_loop_runner::{self, EventLoopProxy, MasonryUserEvent},
|
||||||
|
widget::RootWidget,
|
||||||
|
WidgetId,
|
||||||
|
};
|
||||||
|
use xilem_core::{DynMessage, Message, MessageResult, ProxyError, RawProxy, ViewId};
|
||||||
|
|
||||||
use crate::{ViewCtx, WidgetView};
|
use crate::{ViewCtx, WidgetView};
|
||||||
|
|
||||||
|
@ -10,10 +17,52 @@ pub struct MasonryDriver<State, Logic, View, ViewState> {
|
||||||
pub(crate) state: State,
|
pub(crate) state: State,
|
||||||
pub(crate) logic: Logic,
|
pub(crate) logic: Logic,
|
||||||
pub(crate) current_view: View,
|
pub(crate) current_view: View,
|
||||||
pub(crate) view_ctx: ViewCtx,
|
pub(crate) ctx: ViewCtx,
|
||||||
pub(crate) view_state: ViewState,
|
pub(crate) view_state: ViewState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The `WidgetId` which async events should be sent to.
|
||||||
|
pub const ASYNC_MARKER_WIDGET: WidgetId = WidgetId::reserved(0x1000);
|
||||||
|
|
||||||
|
/// The action which should be used for async events.
|
||||||
|
pub fn async_action(path: Arc<[ViewId]>, message: Box<dyn Message>) -> masonry::Action {
|
||||||
|
masonry::Action::Other(Box::<MessagePackage>::new((path, message)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type used to send a message for async events.
|
||||||
|
type MessagePackage = (Arc<[ViewId]>, DynMessage);
|
||||||
|
|
||||||
|
impl RawProxy for MasonryProxy {
|
||||||
|
fn send_message(&self, path: Arc<[ViewId]>, message: DynMessage) -> Result<(), ProxyError> {
|
||||||
|
match self
|
||||||
|
.0
|
||||||
|
.send_event(event_loop_runner::MasonryUserEvent::Action(
|
||||||
|
async_action(path, message),
|
||||||
|
ASYNC_MARKER_WIDGET,
|
||||||
|
)) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
let MasonryUserEvent::Action(masonry::Action::Other(res), _) = err.0 else {
|
||||||
|
unreachable!(
|
||||||
|
"We know this is the value we just created, which matches this pattern"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
Err(ProxyError::DriverFinished(
|
||||||
|
res.downcast::<MessagePackage>().unwrap().1,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MasonryProxy(pub(crate) EventLoopProxy);
|
||||||
|
|
||||||
|
impl MasonryProxy {
|
||||||
|
pub fn new(proxy: EventLoopProxy) -> Self {
|
||||||
|
Self(proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<State, Logic, View> AppDriver for MasonryDriver<State, Logic, View, View::ViewState>
|
impl<State, Logic, View> AppDriver for MasonryDriver<State, Logic, View, View::ViewState>
|
||||||
where
|
where
|
||||||
Logic: FnMut(&mut State) -> View,
|
Logic: FnMut(&mut State) -> View,
|
||||||
|
@ -21,47 +70,56 @@ where
|
||||||
{
|
{
|
||||||
fn on_action(
|
fn on_action(
|
||||||
&mut self,
|
&mut self,
|
||||||
ctx: &mut masonry::app_driver::DriverCtx<'_>,
|
masonry_ctx: &mut masonry::app_driver::DriverCtx<'_>,
|
||||||
widget_id: masonry::WidgetId,
|
widget_id: masonry::WidgetId,
|
||||||
action: masonry::Action,
|
action: masonry::Action,
|
||||||
) {
|
) {
|
||||||
if let Some(id_path) = self.view_ctx.widget_map.get(&widget_id) {
|
let message_result = if widget_id == ASYNC_MARKER_WIDGET {
|
||||||
let message_result = self.current_view.message(
|
let masonry::Action::Other(action) = action else {
|
||||||
|
panic!();
|
||||||
|
};
|
||||||
|
let (path, message) = *action.downcast::<MessagePackage>().unwrap();
|
||||||
|
// Handle an async path
|
||||||
|
self.current_view
|
||||||
|
.message(&mut self.view_state, &path, message, &mut self.state)
|
||||||
|
} else if let Some(id_path) = self.ctx.widget_map.get(&widget_id) {
|
||||||
|
self.current_view.message(
|
||||||
&mut self.view_state,
|
&mut self.view_state,
|
||||||
id_path.as_slice(),
|
id_path.as_slice(),
|
||||||
Box::new(action),
|
Box::new(action),
|
||||||
&mut self.state,
|
&mut self.state,
|
||||||
);
|
)
|
||||||
let rebuild = match message_result {
|
|
||||||
MessageResult::Action(()) => {
|
|
||||||
// It's not entirely clear what to do here
|
|
||||||
true
|
|
||||||
}
|
|
||||||
MessageResult::RequestRebuild => true,
|
|
||||||
MessageResult::Nop => false,
|
|
||||||
MessageResult::Stale(_) => {
|
|
||||||
tracing::info!("Discarding message");
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if rebuild {
|
|
||||||
let next_view = (self.logic)(&mut self.state);
|
|
||||||
let mut root = ctx.get_root::<RootWidget<View::Widget>>();
|
|
||||||
|
|
||||||
self.view_ctx.view_tree_changed = false;
|
|
||||||
next_view.rebuild(
|
|
||||||
&self.current_view,
|
|
||||||
&mut self.view_state,
|
|
||||||
&mut self.view_ctx,
|
|
||||||
root.get_element(),
|
|
||||||
);
|
|
||||||
if cfg!(debug_assertions) && !self.view_ctx.view_tree_changed {
|
|
||||||
tracing::debug!("Nothing changed as result of action");
|
|
||||||
}
|
|
||||||
self.current_view = next_view;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Got action {action:?} for unknown widget. Did you forget to use `with_action_widget`?");
|
eprintln!("Got action {action:?} for unknown widget. Did you forget to use `with_action_widget`?");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let rebuild = match message_result {
|
||||||
|
MessageResult::Action(()) => {
|
||||||
|
// It's not entirely clear what to do here
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MessageResult::RequestRebuild => true,
|
||||||
|
MessageResult::Nop => false,
|
||||||
|
MessageResult::Stale(_) => {
|
||||||
|
tracing::info!("Discarding message");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if rebuild {
|
||||||
|
let next_view = (self.logic)(&mut self.state);
|
||||||
|
|
||||||
|
let mut root = masonry_ctx.get_root::<RootWidget<View::Widget>>();
|
||||||
|
|
||||||
|
next_view.rebuild(
|
||||||
|
&self.current_view,
|
||||||
|
&mut self.view_state,
|
||||||
|
&mut self.ctx,
|
||||||
|
root.get_element(),
|
||||||
|
);
|
||||||
|
if cfg!(debug_assertions) && !self.ctx.view_tree_changed {
|
||||||
|
tracing::debug!("Nothing changed as result of action");
|
||||||
|
}
|
||||||
|
self.current_view = next_view;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
#![allow(clippy::comparison_chain)]
|
#![allow(clippy::comparison_chain)]
|
||||||
use std::collections::HashMap;
|
#![warn(unnameable_types, unreachable_pub)]
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use driver::MasonryDriver;
|
|
||||||
use masonry::{
|
use masonry::{
|
||||||
dpi::LogicalSize,
|
dpi::LogicalSize,
|
||||||
event_loop_runner,
|
event_loop_runner,
|
||||||
|
@ -15,7 +15,9 @@ use winit::{
|
||||||
error::EventLoopError,
|
error::EventLoopError,
|
||||||
window::{Window, WindowAttributes},
|
window::{Window, WindowAttributes},
|
||||||
};
|
};
|
||||||
use xilem_core::{MessageResult, SuperElement, View, ViewElement, ViewId, ViewPathTracker};
|
use xilem_core::{
|
||||||
|
AsyncCtx, MessageResult, RawProxy, SuperElement, View, ViewElement, ViewId, ViewPathTracker,
|
||||||
|
};
|
||||||
|
|
||||||
pub use masonry::{
|
pub use masonry::{
|
||||||
dpi,
|
dpi,
|
||||||
|
@ -27,36 +29,32 @@ pub use xilem_core as core;
|
||||||
|
|
||||||
mod any_view;
|
mod any_view;
|
||||||
pub use any_view::AnyWidgetView;
|
pub use any_view::AnyWidgetView;
|
||||||
|
|
||||||
mod driver;
|
mod driver;
|
||||||
|
pub use driver::{async_action, MasonryDriver, MasonryProxy, ASYNC_MARKER_WIDGET};
|
||||||
|
|
||||||
pub mod view;
|
pub mod view;
|
||||||
|
|
||||||
pub struct Xilem<State, Logic, View>
|
/// Re-export of tokio as the async driver for Masonry
|
||||||
where
|
pub use tokio;
|
||||||
View: WidgetView<State>,
|
|
||||||
{
|
pub struct Xilem<State, Logic> {
|
||||||
pub root_widget: RootWidget<View::Widget>,
|
state: State,
|
||||||
pub driver: MasonryDriver<State, Logic, View, View::ViewState>,
|
logic: Logic,
|
||||||
|
runtime: tokio::runtime::Runtime,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<State, Logic, View> Xilem<State, Logic, View>
|
impl<State, Logic, View> Xilem<State, Logic>
|
||||||
where
|
where
|
||||||
Logic: FnMut(&mut State) -> View,
|
Logic: FnMut(&mut State) -> View,
|
||||||
View: WidgetView<State>,
|
View: WidgetView<State>,
|
||||||
{
|
{
|
||||||
pub fn new(mut state: State, mut logic: Logic) -> Self {
|
pub fn new(state: State, logic: Logic) -> Self {
|
||||||
let first_view = logic(&mut state);
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||||
let mut view_ctx = ViewCtx::default();
|
|
||||||
let (pod, view_state) = first_view.build(&mut view_ctx);
|
|
||||||
let root_widget = RootWidget::from_pod(pod.inner);
|
|
||||||
Xilem {
|
Xilem {
|
||||||
driver: MasonryDriver {
|
state,
|
||||||
current_view: first_view,
|
logic,
|
||||||
logic,
|
runtime,
|
||||||
state,
|
|
||||||
view_ctx,
|
|
||||||
view_state,
|
|
||||||
},
|
|
||||||
root_widget,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +82,7 @@ where
|
||||||
// TODO: Make windows into a custom view
|
// TODO: Make windows into a custom view
|
||||||
pub fn run_windowed_in(
|
pub fn run_windowed_in(
|
||||||
self,
|
self,
|
||||||
event_loop: EventLoopBuilder,
|
mut event_loop: EventLoopBuilder,
|
||||||
window_attributes: WindowAttributes,
|
window_attributes: WindowAttributes,
|
||||||
) -> Result<(), EventLoopError>
|
) -> Result<(), EventLoopError>
|
||||||
where
|
where
|
||||||
|
@ -92,7 +90,37 @@ where
|
||||||
Logic: 'static,
|
Logic: 'static,
|
||||||
View: 'static,
|
View: 'static,
|
||||||
{
|
{
|
||||||
event_loop_runner::run(event_loop, window_attributes, self.root_widget, self.driver)
|
let event_loop = event_loop.build()?;
|
||||||
|
let proxy = event_loop.create_proxy();
|
||||||
|
let (root_widget, driver) = self.into_driver(Arc::new(MasonryProxy(proxy)));
|
||||||
|
event_loop_runner::run_with(event_loop, window_attributes, root_widget, driver)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_driver(
|
||||||
|
mut self,
|
||||||
|
proxy: Arc<dyn RawProxy>,
|
||||||
|
) -> (
|
||||||
|
impl Widget,
|
||||||
|
MasonryDriver<State, Logic, View, View::ViewState>,
|
||||||
|
) {
|
||||||
|
let first_view = (self.logic)(&mut self.state);
|
||||||
|
let mut ctx = ViewCtx {
|
||||||
|
widget_map: WidgetMap::default(),
|
||||||
|
id_path: Vec::new(),
|
||||||
|
view_tree_changed: false,
|
||||||
|
proxy,
|
||||||
|
runtime: self.runtime,
|
||||||
|
};
|
||||||
|
let (pod, view_state) = first_view.build(&mut ctx);
|
||||||
|
let root_widget = RootWidget::from_pod(pod.inner);
|
||||||
|
let driver = MasonryDriver {
|
||||||
|
current_view: first_view,
|
||||||
|
logic: self.logic,
|
||||||
|
state: self.state,
|
||||||
|
ctx,
|
||||||
|
view_state,
|
||||||
|
};
|
||||||
|
(root_widget, driver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,15 +177,17 @@ where
|
||||||
type Widget = W;
|
type Widget = W;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
type WidgetMap = HashMap<WidgetId, Vec<ViewId>>;
|
||||||
|
|
||||||
pub struct ViewCtx {
|
pub struct ViewCtx {
|
||||||
/// The map from a widgets id to its position in the View tree.
|
/// The map from a widgets id to its position in the View tree.
|
||||||
///
|
///
|
||||||
/// This includes only the widgets which might send actions
|
/// This includes only the widgets which might send actions
|
||||||
/// This is currently never cleaned up
|
widget_map: WidgetMap,
|
||||||
widget_map: HashMap<WidgetId, Vec<ViewId>>,
|
|
||||||
id_path: Vec<ViewId>,
|
id_path: Vec<ViewId>,
|
||||||
view_tree_changed: bool,
|
view_tree_changed: bool,
|
||||||
|
proxy: Arc<dyn RawProxy>,
|
||||||
|
runtime: tokio::runtime::Runtime,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViewPathTracker for ViewCtx {
|
impl ViewPathTracker for ViewCtx {
|
||||||
|
@ -199,4 +229,14 @@ impl ViewCtx {
|
||||||
pub fn teardown_leaf<E: Widget>(&mut self, widget: WidgetMut<E>) {
|
pub fn teardown_leaf<E: Widget>(&mut self, widget: WidgetMut<E>) {
|
||||||
self.widget_map.remove(&widget.ctx.widget_id());
|
self.widget_map.remove(&widget.ctx.widget_id());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn runtime(&self) -> &tokio::runtime::Runtime {
|
||||||
|
&self.runtime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncCtx for ViewCtx {
|
||||||
|
fn proxy(&mut self) -> Arc<dyn RawProxy> {
|
||||||
|
self.proxy.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Copyright 2024 the Xilem Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use std::{future::Future, marker::PhantomData, sync::Arc};
|
||||||
|
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use xilem_core::{DynMessage, Message, MessageProxy, NoElement, View, ViewId, ViewPathTracker};
|
||||||
|
|
||||||
|
use crate::ViewCtx;
|
||||||
|
|
||||||
|
pub fn async_repeat<M, F, H, State, Action, Fut>(
|
||||||
|
future_future: F,
|
||||||
|
on_event: H,
|
||||||
|
) -> AsyncRepeat<F, H, M>
|
||||||
|
where
|
||||||
|
F: Fn(MessageProxy<M>) -> Fut,
|
||||||
|
Fut: Future<Output = ()> + Send + 'static,
|
||||||
|
H: Fn(&mut State, M) -> Action + 'static,
|
||||||
|
M: Message + 'static,
|
||||||
|
{
|
||||||
|
AsyncRepeat {
|
||||||
|
future_future,
|
||||||
|
on_event,
|
||||||
|
message: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AsyncRepeat<F, H, M> {
|
||||||
|
future_future: F,
|
||||||
|
on_event: H,
|
||||||
|
message: PhantomData<fn() -> M>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State, Action, F, H, M, Fut> View<State, Action, ViewCtx> for AsyncRepeat<F, H, M>
|
||||||
|
where
|
||||||
|
F: Fn(MessageProxy<M>) -> Fut + 'static,
|
||||||
|
Fut: Future<Output = ()> + Send + 'static,
|
||||||
|
H: Fn(&mut State, M) -> Action + 'static,
|
||||||
|
M: Message + 'static,
|
||||||
|
{
|
||||||
|
type Element = NoElement;
|
||||||
|
|
||||||
|
type ViewState = JoinHandle<()>;
|
||||||
|
|
||||||
|
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||||
|
let path: Arc<[ViewId]> = ctx.view_path().into();
|
||||||
|
|
||||||
|
let proxy = ctx.proxy.clone();
|
||||||
|
let handle = ctx
|
||||||
|
.runtime()
|
||||||
|
.spawn((self.future_future)(MessageProxy::new(proxy, path)));
|
||||||
|
// TODO: Clearly this shouldn't be a label here
|
||||||
|
(NoElement, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild<'el>(
|
||||||
|
&self,
|
||||||
|
_: &Self,
|
||||||
|
_: &mut Self::ViewState,
|
||||||
|
_: &mut ViewCtx,
|
||||||
|
(): xilem_core::Mut<'el, Self::Element>,
|
||||||
|
) -> xilem_core::Mut<'el, Self::Element> {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
fn teardown(
|
||||||
|
&self,
|
||||||
|
_: &mut Self::ViewState,
|
||||||
|
_: &mut ViewCtx,
|
||||||
|
_: xilem_core::Mut<'_, Self::Element>,
|
||||||
|
) {
|
||||||
|
// Nothing to do
|
||||||
|
// TODO: Our state will be dropped, finishing the future
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message(
|
||||||
|
&self,
|
||||||
|
_: &mut Self::ViewState,
|
||||||
|
id_path: &[xilem_core::ViewId],
|
||||||
|
message: DynMessage,
|
||||||
|
app_state: &mut State,
|
||||||
|
) -> xilem_core::MessageResult<Action> {
|
||||||
|
debug_assert!(
|
||||||
|
id_path.is_empty(),
|
||||||
|
"id path should be empty in AsyncRepeat::message"
|
||||||
|
);
|
||||||
|
let message = message.downcast::<M>().unwrap();
|
||||||
|
xilem_core::MessageResult::Action((self.on_event)(app_state, *message))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
// Copyright 2024 the Xilem Authors
|
// Copyright 2024 the Xilem Authors
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
mod async_repeat;
|
||||||
|
pub use async_repeat::{async_repeat, AsyncRepeat};
|
||||||
|
|
||||||
mod button;
|
mod button;
|
||||||
pub use button::*;
|
pub use button::*;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,10 @@ repository.workspace = true
|
||||||
|
|
||||||
publish = false # We'll publish this alongside Xilem 0.2
|
publish = false # We'll publish this alongside Xilem 0.2
|
||||||
|
|
||||||
|
[features]
|
||||||
|
kurbo = ["dep:kurbo"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
kurbo = { optional = true, workspace = true }
|
kurbo = { optional = true, workspace = true }
|
||||||
|
@ -21,6 +25,4 @@ workspace = true
|
||||||
default-target = "x86_64-unknown-linux-gnu"
|
default-target = "x86_64-unknown-linux-gnu"
|
||||||
# xilem_core is entirely platform-agnostic, so only display docs for one platform
|
# xilem_core is entirely platform-agnostic, so only display docs for one platform
|
||||||
targets = []
|
targets = []
|
||||||
|
features = ["kurbo"]
|
||||||
[features]
|
|
||||||
kurbo = ["dep:kurbo"]
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ If you wish to use Xilem Core in environments where an allocator is not availabl
|
||||||
|
|
||||||
## Minimum supported Rust Version (MSRV)
|
## Minimum supported Rust Version (MSRV)
|
||||||
|
|
||||||
This version of Xilem Core has been verified to compile with **Rust 1.77** and later.
|
This version of Xilem Core has been verified to compile with **Rust 1.79** and later.
|
||||||
|
|
||||||
Future versions of Xilem Core might increase the Rust version requirement.
|
Future versions of Xilem Core might increase the Rust version requirement.
|
||||||
It will not be treated as a breaking change and as such can even happen with small patch releases.
|
It will not be treated as a breaking change and as such can even happen with small patch releases.
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
// Copyright 2024 the Xilem Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use core::{fmt::Display, marker::PhantomData};
|
||||||
|
|
||||||
|
use alloc::{boxed::Box, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{DynMessage, Message, NoElement, View, ViewId, ViewPathTracker};
|
||||||
|
|
||||||
|
/// A `Context` for a [`View`](crate::View) implementation which supports
|
||||||
|
/// asynchronous message reporting.
|
||||||
|
pub trait AsyncCtx<Message = DynMessage>: ViewPathTracker {
|
||||||
|
/// Get a [`Proxy`] for this context.
|
||||||
|
// TODO: Maybe store the current path within this Proxy?
|
||||||
|
fn proxy(&mut self) -> Arc<dyn RawProxy<Message>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handle to a Xilem driver which can be used to queue a message for a View.
|
||||||
|
///
|
||||||
|
/// These messages are [`crate::DynMessage`]s, which are sent to a view at
|
||||||
|
/// a specific path.
|
||||||
|
///
|
||||||
|
/// This can be used for asynchronous event handling.
|
||||||
|
/// For example, to get the result of a `Future` or a channel into
|
||||||
|
/// the view, which then will ultimately.
|
||||||
|
///
|
||||||
|
/// In the Xilem crate, this will wrap an `EventLoopProxy` from Winit.
|
||||||
|
///
|
||||||
|
/// ## Lifetimes
|
||||||
|
///
|
||||||
|
/// It is valid for a [`Proxy`] to outlive the [`View`](crate::View) it is associated with.
|
||||||
|
pub trait RawProxy<Message = DynMessage>: Send + Sync + 'static {
|
||||||
|
/// Send a `message` to the view at `path` in this driver.
|
||||||
|
///
|
||||||
|
/// Note that it is only valid to send messages to views which expect
|
||||||
|
/// them, of the type they expect.
|
||||||
|
/// It is expected for [`View`](crate::View)s to panic otherwise, and the routing
|
||||||
|
/// will prefer to send stable.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This method may error if the driver is no longer running, and in any other
|
||||||
|
/// cases directly documented on the context which was used to create this proxy.
|
||||||
|
/// It may also fail silently.
|
||||||
|
// TODO: Do we want/need a way to asynchronously report errors back to the caller?
|
||||||
|
//
|
||||||
|
// e.g. an `Option<Arc<dyn FnMut(ProxyError, ProxyMessageId?)>>`?
|
||||||
|
fn send_message(&self, path: Arc<[ViewId]>, message: Message) -> Result<(), ProxyError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A way to send a message of an expected type to a specific view.
|
||||||
|
pub struct MessageProxy<M: Message> {
|
||||||
|
proxy: Arc<dyn RawProxy<DynMessage>>,
|
||||||
|
path: Arc<[ViewId]>,
|
||||||
|
message: PhantomData<fn(M)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: Message> MessageProxy<M> {
|
||||||
|
/// Create a new `MessageProxy`
|
||||||
|
pub fn new(proxy: Arc<dyn RawProxy<DynMessage>>, path: Arc<[ViewId]>) -> Self {
|
||||||
|
Self {
|
||||||
|
proxy,
|
||||||
|
path,
|
||||||
|
message: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send `message` to the `View` which created this `MessageProxy`
|
||||||
|
pub fn message(&self, message: M) -> Result<(), ProxyError> {
|
||||||
|
self.proxy
|
||||||
|
.send_message(self.path.clone(), Box::new(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`View`] which has no element type.
|
||||||
|
pub trait PhantomView<State, Action, Context, Message = DynMessage>:
|
||||||
|
View<State, Action, Context, Message, Element = NoElement>
|
||||||
|
where
|
||||||
|
Context: ViewPathTracker,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State, Action, Context, Message, V> PhantomView<State, Action, Context, Message> for V
|
||||||
|
where
|
||||||
|
V: View<State, Action, Context, Message, Element = NoElement>,
|
||||||
|
Context: ViewPathTracker,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The potential error conditions from a [`Proxy`] sending a message
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ProxyError {
|
||||||
|
/// The underlying driver (such as an event loop) is no longer running.
|
||||||
|
///
|
||||||
|
/// TODO: Should this also support a source message?
|
||||||
|
DriverFinished(DynMessage),
|
||||||
|
/// The [`View`](crate::View) the message was being routed to is no longer in the view tree.
|
||||||
|
///
|
||||||
|
/// This likely requires async error handling to happen.
|
||||||
|
ViewExpired(DynMessage, Arc<[ViewId]>),
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
Other(&'static str),
|
||||||
|
// TODO: When core::error::Error is stabilised
|
||||||
|
// Other(Box<dyn core::error::Error + Send>),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is it fine to use thiserror in this crate?
|
||||||
|
impl Display for ProxyError {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match &self {
|
||||||
|
ProxyError::DriverFinished(_) => f.write_fmt(format_args!("the driver finished")),
|
||||||
|
ProxyError::ViewExpired(_, _) => {
|
||||||
|
f.write_fmt(format_args!("the corresponding view is no longer present"))
|
||||||
|
}
|
||||||
|
|
||||||
|
ProxyError::Other(inner) => inner.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl std::error::Error for ProxyError {
|
||||||
|
// fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
// match self {
|
||||||
|
// ProxyError::Other(inner) => inner.source(),
|
||||||
|
// _ => None,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright 2024 the Xilem Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
//! Fake implementations of Xilem traits for use within documentation examples and tests.
|
||||||
|
|
||||||
|
use crate::ViewPathTracker;
|
||||||
|
|
||||||
|
/// A type used for documentation
|
||||||
|
pub enum Fake {}
|
||||||
|
|
||||||
|
impl ViewPathTracker for Fake {
|
||||||
|
fn push_id(&mut self, _: crate::ViewId) {
|
||||||
|
match *self {}
|
||||||
|
}
|
||||||
|
fn pop_id(&mut self) {
|
||||||
|
match *self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_path(&mut self) -> &[crate::ViewId] {
|
||||||
|
match *self {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,3 +82,15 @@ where
|
||||||
/// Replace the inner value of this reference entirely
|
/// Replace the inner value of this reference entirely
|
||||||
fn replace_inner(this: Self::Mut<'_>, child: Child) -> Self::Mut<'_>;
|
fn replace_inner(this: Self::Mut<'_>, child: Child) -> Self::Mut<'_>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Element type for views which don't impact the element tree.
|
||||||
|
///
|
||||||
|
/// Views with this element type can be included in any [`ViewSequence`](crate::ViewSequence) (with the
|
||||||
|
/// correct `State` and `Action` types), as they do not need to actually add an element to the sequence.
|
||||||
|
///
|
||||||
|
/// These views can also as the `alongside_view` in [`fork`](crate::fork).
|
||||||
|
pub struct NoElement;
|
||||||
|
|
||||||
|
impl ViewElement for NoElement {
|
||||||
|
type Mut<'a> = ();
|
||||||
|
}
|
||||||
|
|
|
@ -19,23 +19,28 @@
|
||||||
|
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
|
mod deferred;
|
||||||
|
pub use deferred::{AsyncCtx, MessageProxy, PhantomView, ProxyError, RawProxy};
|
||||||
|
|
||||||
mod view;
|
mod view;
|
||||||
pub use view::{View, ViewId, ViewPathTracker};
|
pub use view::{View, ViewId, ViewPathTracker};
|
||||||
|
|
||||||
mod views;
|
mod views;
|
||||||
pub use views::{
|
pub use views::{
|
||||||
adapt, map_action, map_state, memoize, one_of, Adapt, AdaptThunk, MapAction, MapState, Memoize,
|
adapt, fork, map_action, map_state, memoize, one_of, run_once, run_once_raw, Adapt, AdaptThunk,
|
||||||
OrphanView,
|
Fork, MapAction, MapState, Memoize, OrphanView, RunOnce,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
pub use message::{DynMessage, Message, MessageResult};
|
pub use message::{DynMessage, Message, MessageResult};
|
||||||
|
|
||||||
mod element;
|
mod element;
|
||||||
pub use element::{AnyElement, Mut, SuperElement, ViewElement};
|
pub use element::{AnyElement, Mut, NoElement, SuperElement, ViewElement};
|
||||||
|
|
||||||
mod any_view;
|
mod any_view;
|
||||||
pub use any_view::AnyView;
|
pub use any_view::AnyView;
|
||||||
|
|
||||||
mod sequence;
|
mod sequence;
|
||||||
pub use sequence::{AppendVec, ElementSplice, ViewSequence};
|
pub use sequence::{AppendVec, ElementSplice, ViewSequence};
|
||||||
|
|
||||||
|
pub mod docs;
|
||||||
|
|
|
@ -9,6 +9,7 @@ use core::sync::atomic::Ordering;
|
||||||
use alloc::vec::Drain;
|
use alloc::vec::Drain;
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
|
use crate::element::NoElement;
|
||||||
use crate::{DynMessage, MessageResult, SuperElement, View, ViewElement, ViewId, ViewPathTracker};
|
use crate::{DynMessage, MessageResult, SuperElement, View, ViewElement, ViewId, ViewPathTracker};
|
||||||
|
|
||||||
/// An append only `Vec`.
|
/// An append only `Vec`.
|
||||||
|
@ -194,7 +195,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The state used to implement `ViewSequence` for `Option<impl ViewSequence>`
|
/// The state used to implement `ViewSequence` for `Option<impl ViewSequence>`
|
||||||
#[doc(hidden)] // Implementation detail, public because of trait visibility rules
|
#[allow(unnameable_types)] // Public because of trait visibility rules, but has no public API.
|
||||||
pub struct OptionSeqState<InnerState> {
|
pub struct OptionSeqState<InnerState> {
|
||||||
/// The current state.
|
/// The current state.
|
||||||
///
|
///
|
||||||
|
@ -331,6 +332,57 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A `View` with [no element](crate::NoElement) can be added to any ViewSequence, because it does not use any
|
||||||
|
/// properties of the Element type.
|
||||||
|
impl<State, Action, Context, Element, NoElementView, Message>
|
||||||
|
ViewSequence<State, Action, Context, Element, NoElement, Message> for NoElementView
|
||||||
|
where
|
||||||
|
NoElementView: View<State, Action, Context, Message, Element = NoElement>,
|
||||||
|
Element: ViewElement,
|
||||||
|
Context: ViewPathTracker,
|
||||||
|
{
|
||||||
|
#[doc(hidden)]
|
||||||
|
type SeqState = NoElementView::ViewState;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn seq_build(&self, ctx: &mut Context, _: &mut AppendVec<Element>) -> Self::SeqState {
|
||||||
|
let (NoElement, state) = self.build(ctx);
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn seq_rebuild(
|
||||||
|
&self,
|
||||||
|
prev: &Self,
|
||||||
|
seq_state: &mut Self::SeqState,
|
||||||
|
ctx: &mut Context,
|
||||||
|
_: &mut impl ElementSplice<Element>,
|
||||||
|
) {
|
||||||
|
self.rebuild(prev, seq_state, ctx, ());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn seq_teardown(
|
||||||
|
&self,
|
||||||
|
seq_state: &mut Self::SeqState,
|
||||||
|
ctx: &mut Context,
|
||||||
|
_: &mut impl ElementSplice<Element>,
|
||||||
|
) {
|
||||||
|
self.teardown(seq_state, ctx, ());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn seq_message(
|
||||||
|
&self,
|
||||||
|
seq_state: &mut Self::SeqState,
|
||||||
|
id_path: &[ViewId],
|
||||||
|
message: Message,
|
||||||
|
app_state: &mut State,
|
||||||
|
) -> MessageResult<Action, Message> {
|
||||||
|
self.message(seq_state, id_path, message, app_state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The state used to implement `ViewSequence` for `Vec<impl ViewSequence>`
|
/// The state used to implement `ViewSequence` for `Vec<impl ViewSequence>`
|
||||||
///
|
///
|
||||||
/// We use a generation arena for vector types, with half of the `ViewId` dedicated
|
/// We use a generation arena for vector types, with half of the `ViewId` dedicated
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
// Copyright 2024 the Xilem Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
AppendVec, ElementSplice, Mut, NoElement, View, ViewId, ViewPathTracker, ViewSequence,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Create a view which acts as `active_view`, whilst also running `alongside_view`, without inserting it into the tree.
|
||||||
|
///
|
||||||
|
/// `alongside_view` must be a `ViewSequence` with an element type of [`NoElement`].
|
||||||
|
pub fn fork<Active, Alongside, Marker>(
|
||||||
|
active_view: Active,
|
||||||
|
alongside_view: Alongside,
|
||||||
|
) -> Fork<Active, Alongside, Marker> {
|
||||||
|
Fork {
|
||||||
|
active_view,
|
||||||
|
alongside_view,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The view for [`fork`].
|
||||||
|
pub struct Fork<Active, Alongside, Marker> {
|
||||||
|
active_view: Active,
|
||||||
|
alongside_view: Alongside,
|
||||||
|
marker: PhantomData<Marker>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State, Action, Context, Active, Alongside, Marker, Message>
|
||||||
|
View<State, Action, Context, Message> for Fork<Active, Alongside, Marker>
|
||||||
|
where
|
||||||
|
Active: View<State, Action, Context, Message>,
|
||||||
|
Alongside: ViewSequence<State, Action, Context, NoElement, Marker, Message>,
|
||||||
|
Context: ViewPathTracker,
|
||||||
|
Marker: 'static,
|
||||||
|
{
|
||||||
|
type Element = Active::Element;
|
||||||
|
|
||||||
|
type ViewState = (Active::ViewState, Alongside::SeqState);
|
||||||
|
|
||||||
|
fn build(&self, ctx: &mut Context) -> (Self::Element, Self::ViewState) {
|
||||||
|
let (element, active_state) =
|
||||||
|
ctx.with_id(ViewId::new(0), |ctx| self.active_view.build(ctx));
|
||||||
|
let alongside_state = ctx.with_id(ViewId::new(1), |ctx| {
|
||||||
|
self.alongside_view
|
||||||
|
.seq_build(ctx, &mut AppendVec::default())
|
||||||
|
});
|
||||||
|
(element, (active_state, alongside_state))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild<'el>(
|
||||||
|
&self,
|
||||||
|
prev: &Self,
|
||||||
|
(active_state, alongside_state): &mut Self::ViewState,
|
||||||
|
ctx: &mut Context,
|
||||||
|
element: Mut<'el, Self::Element>,
|
||||||
|
) -> Mut<'el, Self::Element> {
|
||||||
|
let element = ctx.with_id(ViewId::new(0), |ctx| {
|
||||||
|
self.active_view
|
||||||
|
.rebuild(&prev.active_view, active_state, ctx, element)
|
||||||
|
});
|
||||||
|
ctx.with_id(ViewId::new(1), |ctx| {
|
||||||
|
self.alongside_view.seq_rebuild(
|
||||||
|
&prev.alongside_view,
|
||||||
|
alongside_state,
|
||||||
|
ctx,
|
||||||
|
&mut NoElements,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
element
|
||||||
|
}
|
||||||
|
|
||||||
|
fn teardown(
|
||||||
|
&self,
|
||||||
|
(active_state, alongside_state): &mut Self::ViewState,
|
||||||
|
ctx: &mut Context,
|
||||||
|
element: Mut<'_, Self::Element>,
|
||||||
|
) {
|
||||||
|
ctx.with_id(ViewId::new(0), |ctx| {
|
||||||
|
self.alongside_view
|
||||||
|
.seq_teardown(alongside_state, ctx, &mut NoElements);
|
||||||
|
});
|
||||||
|
ctx.with_id(ViewId::new(1), |ctx| {
|
||||||
|
self.active_view.teardown(active_state, ctx, element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message(
|
||||||
|
&self,
|
||||||
|
(active_state, alongside_state): &mut Self::ViewState,
|
||||||
|
id_path: &[crate::ViewId],
|
||||||
|
message: Message,
|
||||||
|
app_state: &mut State,
|
||||||
|
) -> crate::MessageResult<Action, Message> {
|
||||||
|
let (first, id_path) = id_path
|
||||||
|
.split_first()
|
||||||
|
.expect("Id path has elements for Fork");
|
||||||
|
match first.routing_id() {
|
||||||
|
0 => self
|
||||||
|
.active_view
|
||||||
|
.message(active_state, id_path, message, app_state),
|
||||||
|
1 => self
|
||||||
|
.alongside_view
|
||||||
|
.seq_message(alongside_state, id_path, message, app_state),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A stub `ElementSplice` implementation for `NoElement`.
|
||||||
|
///
|
||||||
|
/// We know that none of the methods will be called, because the `ViewSequence`
|
||||||
|
/// implementation for `NoElement` views does not use the provided `elements`.
|
||||||
|
///
|
||||||
|
/// It is technically possible for someone to create an implementation of `ViewSequence`
|
||||||
|
/// which uses a `NoElement` `ElementSplice`. But we don't think that sequence could be meaningful,
|
||||||
|
/// so we still panic in that case.
|
||||||
|
struct NoElements;
|
||||||
|
|
||||||
|
impl ElementSplice<NoElement> for NoElements {
|
||||||
|
fn with_scratch<R>(&mut self, f: impl FnOnce(&mut AppendVec<NoElement>) -> R) -> R {
|
||||||
|
let mut append_vec = AppendVec::default();
|
||||||
|
let ret = f(&mut append_vec);
|
||||||
|
debug_assert!(append_vec.into_inner().is_empty());
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert(&mut self, _: NoElement) {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mutate<R>(&mut self, _: impl FnOnce(<NoElement as crate::ViewElement>::Mut<'_>) -> R) -> R {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip(&mut self, n: usize) {
|
||||||
|
if n > 0 {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete<R>(&mut self, _: impl FnOnce(<NoElement as crate::ViewElement>::Mut<'_>) -> R) -> R {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
// Copyright 2024 the Xilem Authors
|
// Copyright 2024 the Xilem Authors
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
mod run_once;
|
||||||
|
pub use run_once::{run_once, run_once_raw, RunOnce};
|
||||||
|
|
||||||
mod adapt;
|
mod adapt;
|
||||||
pub use adapt::{adapt, Adapt, AdaptThunk};
|
pub use adapt::{adapt, Adapt, AdaptThunk};
|
||||||
|
|
||||||
|
@ -10,6 +13,9 @@ pub use map_state::{map_state, MapState};
|
||||||
mod map_action;
|
mod map_action;
|
||||||
pub use map_action::{map_action, MapAction};
|
pub use map_action::{map_action, MapAction};
|
||||||
|
|
||||||
|
mod fork;
|
||||||
|
pub use fork::{fork, Fork};
|
||||||
|
|
||||||
mod memoize;
|
mod memoize;
|
||||||
pub use memoize::{memoize, Memoize};
|
pub use memoize::{memoize, Memoize};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
// Copyright 2024 the Xilem Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use core::fmt::Debug;
|
||||||
|
|
||||||
|
use crate::{MessageResult, NoElement, View, ViewPathTracker};
|
||||||
|
|
||||||
|
/// A view which executes `once` exactly once.
|
||||||
|
///
|
||||||
|
/// `once` will be called only when the returned view is [built](View::build).
|
||||||
|
///
|
||||||
|
/// This is a [`NoElement`] view, and so should either be used in any sequence, or with [`fork`](crate::fork).
|
||||||
|
///
|
||||||
|
/// ## Examples
|
||||||
|
///
|
||||||
|
/// This can be useful for logging a value:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use xilem_core::{run_once, View, docs::{Fake as ViewCtx}, PhantomView};
|
||||||
|
/// # struct AppData;
|
||||||
|
/// fn log_lifecycle(data: &mut AppData) -> impl PhantomView<AppData, (), ViewCtx> {
|
||||||
|
/// run_once(|| eprintln!("View constructed"))
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// ## Capturing
|
||||||
|
///
|
||||||
|
/// This method cannot be used with a dynamic `once`.
|
||||||
|
/// That is, `once` cannot be a function pointer or capture any (non-zero sized) values.
|
||||||
|
/// You might otherwise expect the function to be reran when the captured values change, which is not the case.
|
||||||
|
/// [`run_once_raw`] is the same as `run_once`, but without this restriction.
|
||||||
|
///
|
||||||
|
/// // https://doc.rust-lang.org/error_codes/E0080.html
|
||||||
|
/// // Note that this error code is only checked on nightly
|
||||||
|
/// ```compile_fail,E0080
|
||||||
|
/// # use xilem_core::{run_once, View, docs::{Fake as ViewCtx}, PhantomView};
|
||||||
|
/// # struct AppData {
|
||||||
|
/// # data: u32
|
||||||
|
/// # }
|
||||||
|
/// fn log_data(app: &mut AppData) -> impl PhantomView<AppData, (), ViewCtx> {
|
||||||
|
/// let val = app.data;
|
||||||
|
/// run_once(move || println!("{}", val))
|
||||||
|
/// }
|
||||||
|
/// # // We need to call the function to make the inline constant be evaluated
|
||||||
|
/// # let _ = log_data(&mut AppData { data: 10 });
|
||||||
|
/// ```
|
||||||
|
pub fn run_once<F>(once: F) -> RunOnce<F>
|
||||||
|
where
|
||||||
|
F: Fn() + 'static,
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
assert!(
|
||||||
|
core::mem::size_of::<F>() == 0,
|
||||||
|
"`run_once` will not be ran again when its captured variables are updated.\n\
|
||||||
|
To ignore this warning, use `run_once_raw`."
|
||||||
|
);
|
||||||
|
};
|
||||||
|
RunOnce { once }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A view which executes `once` exactly once.
|
||||||
|
///
|
||||||
|
/// This is [`run_once`] without the capturing rules.
|
||||||
|
/// See [`run_once`] for full documentation.
|
||||||
|
pub fn run_once_raw<F>(once: F) -> RunOnce<F>
|
||||||
|
where
|
||||||
|
F: Fn() + 'static,
|
||||||
|
{
|
||||||
|
RunOnce { once }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The view type for [`run_once`].
|
||||||
|
///
|
||||||
|
/// This is a [`NoElement`] view.
|
||||||
|
pub struct RunOnce<F> {
|
||||||
|
once: F,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F, State, Action, Context, Message> View<State, Action, Context, Message> for RunOnce<F>
|
||||||
|
where
|
||||||
|
Context: ViewPathTracker,
|
||||||
|
F: Fn() + 'static,
|
||||||
|
// TODO: Work out what traits we want to require `Message`s to have
|
||||||
|
Message: Debug,
|
||||||
|
{
|
||||||
|
type Element = NoElement;
|
||||||
|
|
||||||
|
type ViewState = ();
|
||||||
|
|
||||||
|
fn build(&self, _: &mut Context) -> (Self::Element, Self::ViewState) {
|
||||||
|
(self.once)();
|
||||||
|
(NoElement, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild<'el>(
|
||||||
|
&self,
|
||||||
|
_: &Self,
|
||||||
|
(): &mut Self::ViewState,
|
||||||
|
_: &mut Context,
|
||||||
|
(): crate::Mut<'el, Self::Element>,
|
||||||
|
) -> crate::Mut<'el, Self::Element> {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
fn teardown(
|
||||||
|
&self,
|
||||||
|
(): &mut Self::ViewState,
|
||||||
|
_: &mut Context,
|
||||||
|
_: crate::Mut<'_, Self::Element>,
|
||||||
|
) {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message(
|
||||||
|
&self,
|
||||||
|
(): &mut Self::ViewState,
|
||||||
|
_: &[crate::ViewId],
|
||||||
|
message: Message,
|
||||||
|
_: &mut State,
|
||||||
|
) -> MessageResult<Action, Message> {
|
||||||
|
// Nothing to do
|
||||||
|
panic!("Message should not have been sent to a `RunOnce` View: {message:?}");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue