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:
Daniel McNab 2024-07-18 08:38:28 +01:00 committed by GitHub
parent 732cfa8376
commit 7f40266bd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 944 additions and 144 deletions

View File

@ -8,7 +8,7 @@ env:
# 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,
# 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.
# This should be limited to packages that are intended for publishing.
# If updating, synchronise RUST_MIN_VER_WASM_PKGS

67
Cargo.lock generated
View File

@ -107,6 +107,15 @@ dependencies = [
"winit",
]
[[package]]
name = "addr2line"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
@ -440,6 +449,21 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "bit-set"
version = "0.5.3"
@ -1268,6 +1292,12 @@ dependencies = [
"wasi",
]
[[package]]
name = "gimli"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]]
name = "gl_generator"
version = "0.14.0"
@ -1940,6 +1970,16 @@ dependencies = [
"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]]
name = "num_enum"
version = "0.7.2"
@ -2173,6 +2213,15 @@ dependencies = [
"objc2-foundation",
]
[[package]]
name = "object"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.19.0"
@ -2571,6 +2620,12 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "1.1.0"
@ -3023,6 +3078,17 @@ dependencies = [
"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]]
name = "toml_datetime"
version = "0.6.5"
@ -4005,6 +4071,7 @@ dependencies = [
"accesskit_winit",
"masonry",
"smallvec",
"tokio",
"tracing",
"vello",
"winit",

View File

@ -17,7 +17,7 @@ members = [
[workspace.package]
edition = "2021"
# 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"
repository = "https://github.com/linebender/xilem"
homepage = "https://xilem.dev/"

View File

@ -90,7 +90,7 @@ fn main() {
## 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.
It will not be treated as a breaking change and as such can even happen with small patch releases.

View File

@ -7,8 +7,6 @@
#![windows_subsystem = "windows"]
#![allow(clippy::single_match)]
use std::sync::Arc;
use accesskit::{DefaultActionVerb, Role};
use masonry::app_driver::{AppDriver, DriverCtx};
use masonry::dpi::LogicalSize;
@ -156,7 +154,7 @@ impl Widget for CalcButton {
}
PointerEvent::PointerUp(_, _) => {
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();
trace!("CalcButton {:?} released", ctx.widget_id());
}
@ -176,7 +174,7 @@ impl Widget for CalcButton {
if event.target == ctx.widget_id() {
match event.action {
accesskit::Action::Default => {
ctx.submit_action(Action::Other(Arc::new(self.action)));
ctx.submit_action(Action::Other(Box::new(self.action)));
ctx.request_paint();
}
_ => {}

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
use std::any::Any;
use std::sync::Arc;
use crate::event::PointerButton;
@ -20,7 +19,7 @@ pub enum Action {
TextEntered(String),
CheckboxChecked(bool),
// FIXME - This is a huge hack
Other(Arc<dyn Any + Send + Sync>),
Other(Box<dyn Any + Send>),
}
impl PartialEq for Action {
@ -30,9 +29,8 @@ impl PartialEq for Action {
(Self::TextChanged(l0), Self::TextChanged(r0)) => l0 == r0,
(Self::TextEntered(l0), Self::TextEntered(r0)) => l0 == r0,
(Self::CheckboxChecked(l0), Self::CheckboxChecked(r0)) => l0 == r0,
#[allow(ambiguous_wide_pointer_comparisons)]
// 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,
}
}

View File

@ -16,14 +16,27 @@ use winit::event::{
DeviceEvent as WinitDeviceEvent, DeviceId, MouseButton as WinitMouseButton,
WindowEvent as WinitWindowEvent,
};
use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
use winit::event_loop::ActiveEventLoop;
use winit::window::{Window, WindowAttributes, WindowId};
use crate::app_driver::{AppDriver, DriverCtx};
use crate::dpi::LogicalPosition;
use crate::event::{PointerButton, PointerState, WindowEvent};
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 {
fn from(button: WinitMouseButton) -> Self {
@ -64,7 +77,7 @@ pub struct MasonryState<'a> {
renderer: Option<Renderer>,
// TODO: Winit doesn't seem to let us create these proxies from within the loop
// The reasons for this are unclear
proxy: EventLoopProxy<accesskit_winit::Event>,
proxy: EventLoopProxy,
// Per-Window state
// In future, this will support multiple windows
@ -79,11 +92,14 @@ struct MainState<'a> {
/// 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
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.
///
/// 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 ---
pub fn run(
@ -97,12 +113,12 @@ pub fn run(
) -> Result<(), EventLoopError> {
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(
window: WindowAttributes,
event_loop: EventLoop,
window: WindowAttributes,
root_widget: impl Widget,
app_driver: impl AppDriver + 'static,
) -> Result<(), EventLoopError> {
@ -120,7 +136,7 @@ pub fn run_with(
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) {
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
.handle_user_event(event_loop, event, self.app_driver.as_mut());
}
@ -526,20 +542,29 @@ impl MasonryState<'_> {
pub fn handle_user_event(
&mut self,
event_loop: &ActiveEventLoop,
event: accesskit_winit::Event,
event: MasonryUserEvent,
app_driver: &mut dyn AppDriver,
) {
match event.window_event {
// Note that this event can be called at any time, even multiple times if
// the user restarts their screen reader.
accesskit_winit::WindowEvent::InitialTreeRequested => {
self.render_root
.handle_window_event(WindowEvent::RebuildAccessTree);
match event {
MasonryUserEvent::AccessKit(event) => {
match event.window_event {
// Note that this event can be called at any time, even multiple times if
// the user restarts their screen reader.
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) => {
self.render_root.root_on_access_event(action_request);
}
accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
MasonryUserEvent::Action(action, widget) => self
.render_root
.state
.signal_queue
.push_back(render_root::RenderRootSignal::Action(action, widget)),
}
self.handle_signals(event_loop, app_driver);

View File

@ -107,6 +107,7 @@ impl WidgetMut<'_, Label> {
let ret = f(&mut self.widget.text_layout);
if self.widget.text_layout.needs_rebuild() {
self.ctx.request_layout();
self.ctx.request_paint();
}
ret
}
@ -151,7 +152,7 @@ impl Widget for Label {
// TODO: Set cursor if over link
}
PointerEvent::PointerDown(_button, _state) => {
// TODO: Start tracking currently pressed link
// TODO: Start tracking currently pressed
// (i.e. don't press)
}
PointerEvent::PointerUp(_button, _state) => {

View File

@ -36,6 +36,7 @@ vello.workspace = true
smallvec.workspace = true
accesskit.workspace = true
accesskit_winit.workspace = true
tokio = { version = "1.38.0", features = ["rt", "rt-multi-thread", "time"] }
[target.'cfg(target_os = "android")'.dev-dependencies]
winit = { features = ["android-native-activity"], workspace = true }

View File

@ -33,7 +33,7 @@ Lots of things need improvements.
## 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.
It will not be treated as a breaking change and as such can even happen with small patch releases.

View File

@ -5,8 +5,11 @@
//! Currently, this supports running as its own window alongside an existing application, or
//! accessing raw events from winit.
//! Support for more custom embeddings would be welcome, but needs more design work
use std::sync::Arc;
use masonry::{
app_driver::AppDriver,
event_loop_runner::MasonryUserEvent,
widget::{CrossAxisAlignment, MainAxisAlignment},
ArcStr,
};
@ -18,7 +21,7 @@ use winit::{
};
use xilem::{
view::{button, flex, label, sized_box},
EventLoop, WidgetView, Xilem,
EventLoop, MasonryProxy, WidgetView, Xilem,
};
/// A component to make a bigger than usual button
@ -50,7 +53,7 @@ struct ExternalApp {
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) {
self.masonry_state.handle_resumed(event_loop);
}
@ -80,7 +83,7 @@ impl ApplicationHandler<accesskit_winit::Event> for ExternalApp {
fn user_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
event: accesskit_winit::Event,
event: MasonryUserEvent,
) {
self.masonry_state
.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 event_loop = EventLoop::with_user_event().build().unwrap();
let masonry_state = masonry::event_loop_runner::MasonryState::new(
window_attributes,
&event_loop,
xilem.root_widget,
);
let proxy = MasonryProxy::new(event_loop.create_proxy());
let (widget, driver) = xilem.into_driver(Arc::new(proxy));
let masonry_state =
masonry::event_loop_runner::MasonryState::new(window_attributes, &event_loop, widget);
let mut app = ExternalApp {
masonry_state,
app_driver: Box::new(xilem.driver),
app_driver: Box::new(driver),
};
event_loop.run_app(&mut app)
}

View File

@ -4,8 +4,11 @@
// On Windows platform, don't show a console when opening the app.
#![windows_subsystem = "windows"]
use std::time::Duration;
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,
};
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)
.map(|x| button(format!("+{x}"), move |data: &mut AppData| data.count += x))
.collect::<Vec<_>>();
flex((
fork(
flex((
label("Label")
.brush(Color::REBECCA_PURPLE)
.alignment(TextAlignment::Start),
// TODO masonry doesn't allow setting disabled manually anymore?
// label("Disabled label").disabled(),
))
.direction(Axis::Horizontal),
flex(textbox(
data.textbox_contents.clone(),
|data: &mut AppData, new_value| {
data.textbox_contents = new_value;
flex((
label("Label")
.brush(Color::REBECCA_PURPLE)
.alignment(TextAlignment::Start),
// TODO masonry doesn't allow setting disabled manually anymore?
// label("Disabled label").disabled(),
))
.direction(Axis::Horizontal),
flex(textbox(
data.textbox_contents.clone(),
|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;
};
}
},
))
.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),
))
|data: &mut AppData, ()| data.count += 1,
),
)
}
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| {
data.count = -1_000_000;
}),
run_once(|| tracing::warn!("The pathway to unlimited power has been revealed")),
))
.direction(Axis::Horizontal),
)
@ -116,6 +134,7 @@ fn main() {
#[cfg(target_os = "android")]
use winit::platform::android::activity::AndroidApp;
use xilem_core::{fork, run_once};
#[cfg(target_os = "android")]
// Safety: We are following `android_activity`'s docs here

View File

@ -56,6 +56,7 @@ impl<W: Widget> AnyElement<Pod<W>> for Pod<DynWidget> {
/// A widget whose only child can be dynamically replaced.
///
/// `WidgetPod<Box<dyn Widget>>` doesn't expose this possibility.
#[allow(unnameable_types)] // This is an implementation detail of `AnyWidgetView`
pub struct DynWidget {
inner: WidgetPod<Box<dyn Widget>>,
}

View File

@ -1,8 +1,15 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
use masonry::{app_driver::AppDriver, widget::RootWidget};
use xilem_core::MessageResult;
use std::sync::Arc;
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};
@ -10,10 +17,52 @@ pub struct MasonryDriver<State, Logic, View, ViewState> {
pub(crate) state: State,
pub(crate) logic: Logic,
pub(crate) current_view: View,
pub(crate) view_ctx: ViewCtx,
pub(crate) ctx: ViewCtx,
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>
where
Logic: FnMut(&mut State) -> View,
@ -21,47 +70,56 @@ where
{
fn on_action(
&mut self,
ctx: &mut masonry::app_driver::DriverCtx<'_>,
masonry_ctx: &mut masonry::app_driver::DriverCtx<'_>,
widget_id: masonry::WidgetId,
action: masonry::Action,
) {
if let Some(id_path) = self.view_ctx.widget_map.get(&widget_id) {
let message_result = self.current_view.message(
let message_result = if widget_id == ASYNC_MARKER_WIDGET {
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,
id_path.as_slice(),
Box::new(action),
&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 {
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;
}
}
}

View File

@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
#![allow(clippy::comparison_chain)]
use std::collections::HashMap;
#![warn(unnameable_types, unreachable_pub)]
use std::{collections::HashMap, sync::Arc};
use driver::MasonryDriver;
use masonry::{
dpi::LogicalSize,
event_loop_runner,
@ -15,7 +15,9 @@ use winit::{
error::EventLoopError,
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::{
dpi,
@ -27,36 +29,32 @@ pub use xilem_core as core;
mod any_view;
pub use any_view::AnyWidgetView;
mod driver;
pub use driver::{async_action, MasonryDriver, MasonryProxy, ASYNC_MARKER_WIDGET};
pub mod view;
pub struct Xilem<State, Logic, View>
where
View: WidgetView<State>,
{
pub root_widget: RootWidget<View::Widget>,
pub driver: MasonryDriver<State, Logic, View, View::ViewState>,
/// Re-export of tokio as the async driver for Masonry
pub use tokio;
pub struct Xilem<State, Logic> {
state: State,
logic: Logic,
runtime: tokio::runtime::Runtime,
}
impl<State, Logic, View> Xilem<State, Logic, View>
impl<State, Logic, View> Xilem<State, Logic>
where
Logic: FnMut(&mut State) -> View,
View: WidgetView<State>,
{
pub fn new(mut state: State, mut logic: Logic) -> Self {
let first_view = logic(&mut state);
let mut view_ctx = ViewCtx::default();
let (pod, view_state) = first_view.build(&mut view_ctx);
let root_widget = RootWidget::from_pod(pod.inner);
pub fn new(state: State, logic: Logic) -> Self {
let runtime = tokio::runtime::Runtime::new().unwrap();
Xilem {
driver: MasonryDriver {
current_view: first_view,
logic,
state,
view_ctx,
view_state,
},
root_widget,
state,
logic,
runtime,
}
}
@ -84,7 +82,7 @@ where
// TODO: Make windows into a custom view
pub fn run_windowed_in(
self,
event_loop: EventLoopBuilder,
mut event_loop: EventLoopBuilder,
window_attributes: WindowAttributes,
) -> Result<(), EventLoopError>
where
@ -92,7 +90,37 @@ where
Logic: '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;
}
#[derive(Default)]
type WidgetMap = HashMap<WidgetId, Vec<ViewId>>;
pub struct ViewCtx {
/// The map from a widgets id to its position in the View tree.
///
/// This includes only the widgets which might send actions
/// This is currently never cleaned up
widget_map: HashMap<WidgetId, Vec<ViewId>>,
widget_map: WidgetMap,
id_path: Vec<ViewId>,
view_tree_changed: bool,
proxy: Arc<dyn RawProxy>,
runtime: tokio::runtime::Runtime,
}
impl ViewPathTracker for ViewCtx {
@ -199,4 +229,14 @@ impl ViewCtx {
pub fn teardown_leaf<E: Widget>(&mut self, widget: WidgetMut<E>) {
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()
}
}

View File

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

View File

@ -1,6 +1,9 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
mod async_repeat;
pub use async_repeat::{async_repeat, AsyncRepeat};
mod button;
pub use button::*;

View File

@ -10,6 +10,10 @@ repository.workspace = true
publish = false # We'll publish this alongside Xilem 0.2
[features]
kurbo = ["dep:kurbo"]
[dependencies]
tracing.workspace = true
kurbo = { optional = true, workspace = true }
@ -21,6 +25,4 @@ workspace = true
default-target = "x86_64-unknown-linux-gnu"
# xilem_core is entirely platform-agnostic, so only display docs for one platform
targets = []
[features]
kurbo = ["dep:kurbo"]
features = ["kurbo"]

View File

@ -47,7 +47,7 @@ If you wish to use Xilem Core in environments where an allocator is not availabl
## 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.
It will not be treated as a breaking change and as such can even happen with small patch releases.

128
xilem_core/src/deferred.rs Normal file
View File

@ -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,
// }
// }
// }

22
xilem_core/src/docs.rs Normal file
View File

@ -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 {}
}
}

View File

@ -82,3 +82,15 @@ where
/// Replace the inner value of this reference entirely
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> = ();
}

View File

@ -19,23 +19,28 @@
extern crate alloc;
mod deferred;
pub use deferred::{AsyncCtx, MessageProxy, PhantomView, ProxyError, RawProxy};
mod view;
pub use view::{View, ViewId, ViewPathTracker};
mod views;
pub use views::{
adapt, map_action, map_state, memoize, one_of, Adapt, AdaptThunk, MapAction, MapState, Memoize,
OrphanView,
adapt, fork, map_action, map_state, memoize, one_of, run_once, run_once_raw, Adapt, AdaptThunk,
Fork, MapAction, MapState, Memoize, OrphanView, RunOnce,
};
mod message;
pub use message::{DynMessage, Message, MessageResult};
mod element;
pub use element::{AnyElement, Mut, SuperElement, ViewElement};
pub use element::{AnyElement, Mut, NoElement, SuperElement, ViewElement};
mod any_view;
pub use any_view::AnyView;
mod sequence;
pub use sequence::{AppendVec, ElementSplice, ViewSequence};
pub mod docs;

View File

@ -9,6 +9,7 @@ use core::sync::atomic::Ordering;
use alloc::vec::Drain;
use alloc::vec::Vec;
use crate::element::NoElement;
use crate::{DynMessage, MessageResult, SuperElement, View, ViewElement, ViewId, ViewPathTracker};
/// An append only `Vec`.
@ -194,7 +195,7 @@ where
}
/// 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> {
/// 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>`
///
/// We use a generation arena for vector types, with half of the `ViewId` dedicated

View File

@ -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!()
}
}

View File

@ -1,6 +1,9 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
mod run_once;
pub use run_once::{run_once, run_once_raw, RunOnce};
mod adapt;
pub use adapt::{adapt, Adapt, AdaptThunk};
@ -10,6 +13,9 @@ pub use map_state::{map_state, MapState};
mod map_action;
pub use map_action::{map_action, MapAction};
mod fork;
pub use fork::{fork, Fork};
mod memoize;
pub use memoize::{memoize, Memoize};

View File

@ -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:?}");
}
}