Move tester to a new iced_tester subcrate

This commit is contained in:
Héctor Ramón Jiménez 2025-08-29 08:39:44 +02:00
parent 9e81c2b9e8
commit 4f7444bddf
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
28 changed files with 392 additions and 355 deletions

13
Cargo.lock generated
View file

@ -2492,6 +2492,7 @@ dependencies = [
"iced_highlighter", "iced_highlighter",
"iced_renderer", "iced_renderer",
"iced_runtime", "iced_runtime",
"iced_tester",
"iced_wgpu", "iced_wgpu",
"iced_widget", "iced_widget",
"iced_winit", "iced_winit",
@ -2548,10 +2549,8 @@ version = "0.14.0-dev"
dependencies = [ dependencies = [
"iced_debug", "iced_debug",
"iced_program", "iced_program",
"iced_test",
"iced_widget", "iced_widget",
"log", "log",
"rfd 0.15.4",
] ]
[[package]] [[package]]
@ -2650,6 +2649,16 @@ dependencies = [
"thiserror 2.0.16", "thiserror 2.0.16",
] ]
[[package]]
name = "iced_tester"
version = "0.14.0-dev"
dependencies = [
"iced_test",
"iced_widget",
"log",
"rfd 0.15.4",
]
[[package]] [[package]]
name = "iced_tiny_skia" name = "iced_tiny_skia"
version = "0.14.0-dev" version = "0.14.0-dev"

View file

@ -42,13 +42,13 @@ markdown = ["iced_widget/markdown"]
# Enables lazy widgets # Enables lazy widgets
lazy = ["iced_widget/lazy"] lazy = ["iced_widget/lazy"]
# Enables debug metrics in native platforms (press F12) # Enables debug metrics in native platforms (press F12)
debug = ["iced_winit/debug", "iced_devtools"] debug = ["iced_winit/debug", "dep:iced_devtools"]
# Enables time-travel debugging (very experimental!) # Enables time-travel debugging (very experimental!)
time-travel = ["debug", "iced_devtools/time-travel"] time-travel = ["debug", "iced_devtools/time-travel"]
# Enables hot reloading (very experimental!) # Enables hot reloading (very experimental!)
hot = ["debug", "iced_debug/hot"] hot = ["debug", "iced_debug/hot"]
# Enables the tester developer tool for recording and playing tests (press F12) # Enables the tester developer tool for recording and playing tests (press F12)
tester = ["debug", "iced_devtools/tester"] tester = ["dep:iced_tester"]
# Enables the `thread-pool` futures executor as the `executor::Default` on native platforms # Enables the `thread-pool` futures executor as the `executor::Default` on native platforms
thread-pool = ["iced_futures/thread-pool"] thread-pool = ["iced_futures/thread-pool"]
# Enables `tokio` as the `executor::Default` on native platforms # Enables `tokio` as the `executor::Default` on native platforms
@ -92,6 +92,9 @@ iced_winit.workspace = true
iced_devtools.workspace = true iced_devtools.workspace = true
iced_devtools.optional = true iced_devtools.optional = true
iced_tester.workspace = true
iced_tester.optional = true
iced_highlighter.workspace = true iced_highlighter.workspace = true
iced_highlighter.optional = true iced_highlighter.optional = true
@ -133,6 +136,7 @@ members = [
"runtime", "runtime",
"selector", "selector",
"test", "test",
"tester",
"tiny_skia", "tiny_skia",
"wgpu", "wgpu",
"widget", "widget",
@ -165,6 +169,7 @@ iced_renderer = { version = "0.14.0-dev", path = "renderer" }
iced_runtime = { version = "0.14.0-dev", path = "runtime" } iced_runtime = { version = "0.14.0-dev", path = "runtime" }
iced_selector = { version = "0.14.0-dev", path = "selector" } iced_selector = { version = "0.14.0-dev", path = "selector" }
iced_test = { version = "0.14.0-dev", path = "test" } iced_test = { version = "0.14.0-dev", path = "test" }
iced_tester = { version = "0.14.0-dev", path = "tester" }
iced_tiny_skia = { version = "0.14.0-dev", path = "tiny_skia" } iced_tiny_skia = { version = "0.14.0-dev", path = "tiny_skia" }
iced_wgpu = { version = "0.14.0-dev", path = "wgpu" } iced_wgpu = { version = "0.14.0-dev", path = "wgpu" }
iced_widget = { version = "0.14.0-dev", path = "widget" } iced_widget = { version = "0.14.0-dev", path = "widget" }

View file

@ -15,16 +15,11 @@ workspace = true
[features] [features]
time-travel = ["iced_program/time-travel"] time-travel = ["iced_program/time-travel"]
tester = ["dep:iced_test", "dep:rfd"]
[dependencies] [dependencies]
iced_debug.workspace = true iced_debug.workspace = true
iced_program.workspace = true
iced_widget.workspace = true iced_widget.workspace = true
log.workspace = true log.workspace = true
iced_test.workspace = true iced_program.workspace = true
iced_test.optional = true iced_program.features = ["debug"]
rfd.workspace = true
rfd.optional = true

View file

@ -1,5 +1,4 @@
use crate::executor; use crate::runtime::task::{self, Task};
use crate::runtime::Task;
use std::process; use std::process;
@ -7,7 +6,7 @@ pub const COMPATIBLE_REVISION: &str =
"20f9c9a897fecac5dce0977bbb5639fdce1f54b9"; "20f9c9a897fecac5dce0977bbb5639fdce1f54b9";
pub fn launch() -> Task<launch::Result> { pub fn launch() -> Task<launch::Result> {
executor::try_spawn_blocking(|mut sender| { task::try_blocking(|mut sender| {
let cargo_install = process::Command::new("cargo") let cargo_install = process::Command::new("cargo")
.args(["install", "--list"]) .args(["install", "--list"])
.output()?; .output()?;
@ -48,7 +47,7 @@ pub fn launch() -> Task<launch::Result> {
} }
pub fn install() -> Task<install::Result> { pub fn install() -> Task<install::Result> {
executor::try_spawn_blocking(|mut sender| { task::try_blocking(|mut sender| {
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};

View file

@ -1,43 +0,0 @@
use crate::futures::futures::channel::mpsc;
use crate::futures::futures::channel::oneshot;
use crate::futures::futures::stream::{self, StreamExt};
use crate::runtime::Task;
use std::thread;
pub fn spawn_blocking<T>(
f: impl FnOnce(mpsc::Sender<T>) + Send + 'static,
) -> Task<T>
where
T: Send + 'static,
{
let (sender, receiver) = mpsc::channel(1);
let _ = thread::spawn(move || {
f(sender);
});
Task::stream(receiver)
}
pub fn try_spawn_blocking<T, E>(
f: impl FnOnce(mpsc::Sender<T>) -> Result<(), E> + Send + 'static,
) -> Task<Result<T, E>>
where
T: Send + 'static,
E: Send + 'static,
{
let (sender, receiver) = mpsc::channel(1);
let (error_sender, error_receiver) = oneshot::channel();
let _ = thread::spawn(move || {
if let Err(error) = f(sender) {
let _ = error_sender.send(Err(error));
}
});
Task::stream(stream::select(
receiver.map(Ok),
stream::once(error_receiver).filter_map(async |result| result.ok()),
))
}

View file

@ -3,39 +3,28 @@ use iced_debug as debug;
use iced_program as program; use iced_program as program;
use iced_program::runtime; use iced_program::runtime;
use iced_program::runtime::futures; use iced_program::runtime::futures;
#[cfg(feature = "tester")] use iced_widget as widget;
use iced_test as test;
use iced_widget::core; use iced_widget::core;
mod comet; mod comet;
mod executor;
mod icon;
mod time_machine; mod time_machine;
mod widget;
#[cfg(feature = "tester")]
mod tester;
#[cfg(not(feature = "tester"))]
#[path = "tester/null.rs"]
mod tester;
use crate::tester::Tester;
use crate::core::border; use crate::core::border;
use crate::core::keyboard; use crate::core::keyboard;
use crate::core::theme::{self, Base, Theme}; use crate::core::theme::{self, Base, Theme};
use crate::core::time::seconds; use crate::core::time::seconds;
use crate::core::window; use crate::core::window;
use crate::core::{Alignment::Center, Color, Element, Length::Fill}; use crate::core::{
Alignment::Center, Color, Element, Font, Length::Fill, Settings,
};
use crate::futures::Subscription; use crate::futures::Subscription;
use crate::program::Program; use crate::program::Program;
use crate::runtime::Task; use crate::program::message;
use crate::runtime::font; use crate::runtime::task::{self, Task};
use crate::time_machine::TimeMachine; use crate::time_machine::TimeMachine;
use crate::widget::{ use crate::widget::{
bottom_right, button, center, column, container, horizontal_space, bottom_right, button, center, column, container, horizontal_space, opaque,
monospace, opaque, row, scrollable, stack, text, themer, row, scrollable, stack, text, themer,
}; };
use std::fmt; use std::fmt;
@ -55,6 +44,7 @@ pub struct Attach<P> {
impl<P> Program for Attach<P> impl<P> Program for Attach<P>
where where
P: Program + 'static, P: Program + 'static,
P::Message: std::fmt::Debug + message::MaybeClone,
{ {
type State = DevTools<P>; type State = DevTools<P>;
type Message = Event<P>; type Message = Event<P>;
@ -66,21 +56,21 @@ where
P::name() P::name()
} }
fn settings(&self) -> core::Settings { fn settings(&self) -> Settings {
self.program.settings() self.program.settings()
} }
fn window(&self) -> Option<window::Settings> {
self.program.window()
}
fn boot(&self) -> (Self::State, Task<Self::Message>) { fn boot(&self) -> (Self::State, Task<Self::Message>) {
let (state, boot) = self.program.boot(); let (state, boot) = self.program.boot();
let (devtools, task) = DevTools::new(state); let (devtools, task) = DevTools::new(state);
( (
devtools, devtools,
Task::batch([ Task::batch([boot.map(Event::Program), task.map(Event::Message)]),
boot.map(Event::Program),
task.map(Event::Message),
font::load(icon::FONT).discard(),
]),
) )
} }
@ -130,7 +120,7 @@ where
state: P::State, state: P::State,
show_notification: bool, show_notification: bool,
time_machine: TimeMachine<P>, time_machine: TimeMachine<P>,
mode: Mode<P>, mode: Mode,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -141,13 +131,10 @@ pub enum Message {
InstallComet, InstallComet,
Installing(comet::install::Result), Installing(comet::install::Result),
CancelSetup, CancelSetup,
Toggle,
Tester(tester::Message),
} }
enum Mode<P: Program> { enum Mode {
Hidden, Hidden,
Open { tester: Tester<P> },
Setup(Setup), Setup(Setup),
} }
@ -164,6 +151,7 @@ enum Goal {
impl<P> DevTools<P> impl<P> DevTools<P>
where where
P: Program + 'static, P: Program + 'static,
P::Message: std::fmt::Debug + message::MaybeClone,
{ {
pub fn new(state: P::State) -> (Self, Task<Message>) { pub fn new(state: P::State) -> (Self, Task<Message>) {
( (
@ -173,7 +161,7 @@ where
show_notification: true, show_notification: true,
time_machine: TimeMachine::new(), time_machine: TimeMachine::new(),
}, },
executor::spawn_blocking(|mut sender| { task::blocking(|mut sender| {
thread::sleep(seconds(2)); thread::sleep(seconds(2));
let _ = sender.try_send(()); let _ = sender.try_send(());
}) })
@ -193,21 +181,6 @@ where
Task::none() Task::none()
} }
Message::Toggle => {
match &self.mode {
Mode::Hidden => {
self.mode = Mode::Open {
tester: Tester::new(program),
};
}
Mode::Open { tester } if !tester.is_busy() => {
self.mode = Mode::Hidden;
}
Mode::Setup(_) | Mode::Open { .. } => {}
}
Task::none()
}
Message::ToggleComet => { Message::ToggleComet => {
if let Mode::Setup(setup) = &self.mode { if let Mode::Setup(setup) = &self.mode {
if matches!(setup, Setup::Idle { .. }) { if matches!(setup, Setup::Idle { .. }) {
@ -290,13 +263,6 @@ where
Task::none() Task::none()
} }
Message::Tester(message) => {
let Mode::Open { tester } = &mut self.mode else {
return Task::none();
};
tester.update(program, message).map(Event::Tester)
}
}, },
Event::Program(message) => { Event::Program(message) => {
self.time_machine.push(&message); self.time_machine.push(&message);
@ -328,13 +294,6 @@ where
Task::none() Task::none()
} }
Event::Tester(tick) => {
let Mode::Open { tester } = &mut self.mode else {
return Task::none();
};
tester.tick(program, tick).map(Event::Tester)
}
Event::Discard => Task::none(), Event::Discard => Task::none(),
} }
} }
@ -347,23 +306,15 @@ where
let state = self.state(); let state = self.state();
let view = { let view = {
let view = || { let theme = program.theme(state, window);
let theme = program.theme(state, window);
let view: Element<'_, _, Theme, _> =
themer(theme, program.view(&self.state, window)).into();
if self.time_machine.is_rewinding() { let view: Element<'_, _, Theme, _> =
view.map(|_| Event::Discard) themer(theme, program.view(state, window)).into();
} else {
view.map(Event::Program)
}
};
match &self.mode { if self.time_machine.is_rewinding() {
Mode::Open { tester } => { view.map(|_| Event::Discard)
tester.view(program, view, Event::Tester) } else {
} view.map(Event::Program)
_ => view(),
} }
}; };
@ -408,28 +359,9 @@ where
}) })
}); });
let sidebar = if let Mode::Open { tester } = &self.mode {
let title = monospace("Developer Tools");
let tester = tester.controls().map(Message::Tester);
let tools = column![title, tester].spacing(10);
let sidebar = container(tools)
.padding(10)
.width(250)
.height(Fill)
.style(container::dark);
Some(Element::from(sidebar).map(Event::Message))
} else {
None
};
let content = row![view, sidebar];
themer( themer(
theme, theme,
stack![content] stack![view]
.height(Fill) .height(Fill)
.push_maybe(setup.map(opaque)) .push_maybe(setup.map(opaque))
.push_maybe(notification.map(|notification| { .push_maybe(notification.map(|notification| {
@ -451,14 +383,6 @@ where
let hotkeys = let hotkeys =
futures::keyboard::on_key_press(|key, _modifiers| match key { futures::keyboard::on_key_press(|key, _modifiers| match key {
keyboard::Key::Named(keyboard::key::Named::F12) => { keyboard::Key::Named(keyboard::key::Named::F12) => {
Some(if cfg!(feature = "tester") {
Message::Toggle
} else {
Message::ToggleComet
})
}
#[cfg(feature = "tester")]
keyboard::Key::Named(keyboard::key::Named::F11) => {
Some(Message::ToggleComet) Some(Message::ToggleComet)
} }
_ => None, _ => None,
@ -479,11 +403,7 @@ where
} }
pub fn scale_factor(&self, program: &P, window: window::Id) -> f64 { pub fn scale_factor(&self, program: &P, window: window::Id) -> f64 {
if let Mode::Open { .. } = &self.mode { program.scale_factor(self.state(), window)
1.0
} else {
program.scale_factor(self.state(), window)
}
} }
pub fn state(&self) -> &P::State { pub fn state(&self) -> &P::State {
@ -497,7 +417,6 @@ where
{ {
Message(Message), Message(Message),
Program(P::Message), Program(P::Message),
Tester(tester::Tick<P>),
Command(debug::Command), Command(debug::Command),
Discard, Discard,
} }
@ -505,34 +424,18 @@ where
impl<P> fmt::Debug for Event<P> impl<P> fmt::Debug for Event<P>
where where
P: Program, P: Program,
P::Message: std::fmt::Debug,
{ {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Message(message) => message.fmt(f), Self::Message(message) => message.fmt(f),
Self::Program(message) => message.fmt(f), Self::Program(message) => message.fmt(f),
Self::Tester(_) => f.write_str("Tester"),
Self::Command(command) => command.fmt(f), Self::Command(command) => command.fmt(f),
Self::Discard => f.write_str("Discard"), Self::Discard => f.write_str("Discard"),
} }
} }
} }
#[cfg(feature = "time-travel")]
impl<P> Clone for Event<P>
where
P: Program,
{
fn clone(&self) -> Self {
match self {
Self::Message(message) => Self::Message(message.clone()),
Self::Program(message) => Self::Program(message.clone()),
Self::Command(command) => Self::Command(*command),
Self::Tester(_) => Self::Discard, // Time traveling an emulator?!
Self::Discard => Self::Discard,
}
}
}
fn setup<Renderer>(goal: &Goal) -> Element<'_, Message, Theme, Renderer> fn setup<Renderer>(goal: &Goal) -> Element<'_, Message, Theme, Renderer>
where where
Renderer: program::Renderer + 'static, Renderer: program::Renderer + 'static,
@ -557,13 +460,14 @@ where
]; ];
let command = container( let command = container(
monospace(format!( text!(
"cargo install --locked \\ "cargo install --locked \\
--git https://github.com/iced-rs/comet.git \\ --git https://github.com/iced-rs/comet.git \\
--rev {}", --rev {}",
comet::COMPATIBLE_REVISION comet::COMPATIBLE_REVISION
)) )
.size(14), .size(14)
.font(Font::MONOSPACE),
) )
.width(Fill) .width(Fill)
.padding(5) .padding(5)
@ -630,9 +534,9 @@ where
text("Installing comet...").size(20), text("Installing comet...").size(20),
container( container(
scrollable( scrollable(
column( column(logs.iter().map(|log| {
logs.iter().map(|log| { monospace(log).size(12).into() }), text(log).size(12).font(Font::MONOSPACE).into()
) }))
.spacing(3), .spacing(3),
) )
.spacing(10) .spacing(10)
@ -653,7 +557,7 @@ fn inline_code<'a, Renderer>(
where where
Renderer: program::Renderer + 'a, Renderer: program::Renderer + 'a,
{ {
container(monospace(code).size(12)) container(text(code).size(12).font(Font::MONOSPACE))
.style(|_theme| { .style(|_theme| {
container::Style::default() container::Style::default()
.background(Color::BLACK) .background(Color::BLACK)

View file

@ -1,49 +0,0 @@
use crate::Program;
use crate::core::{Element, Theme};
use crate::runtime::Task;
use crate::widget::horizontal_space;
use std::marker::PhantomData;
pub struct Tester<P: Program> {
_type: PhantomData<P::Message>,
}
#[derive(Debug, Clone)]
pub enum Message {}
#[allow(missing_debug_implementations)]
pub struct Tick<P: Program> {
_type: PhantomData<P::Message>,
}
impl<P: Program> Tester<P> {
pub fn new(_program: &P) -> Self {
Self { _type: PhantomData }
}
pub fn is_busy(&self) -> bool {
false
}
pub fn update(&mut self, _program: &P, _message: Message) -> Task<Tick<P>> {
Task::none()
}
pub fn tick(&mut self, _program: &P, _tick: Tick<P>) -> Task<Tick<P>> {
Task::none()
}
pub fn view<'a, T: 'static>(
&'a self,
_program: &P,
_current: impl FnOnce() -> Element<'a, T, Theme, P::Renderer>,
_emulate: impl Fn(Tick<P>) -> T + 'a,
) -> Element<'a, T, Theme, P::Renderer> {
horizontal_space().into()
}
pub fn controls(&self) -> Element<'_, Message, Theme, P::Renderer> {
horizontal_space().into()
}
}

View file

@ -13,6 +13,7 @@ where
impl<P> TimeMachine<P> impl<P> TimeMachine<P>
where where
P: Program, P: Program,
P::Message: std::fmt::Debug + Clone,
{ {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {

View file

@ -1,13 +0,0 @@
pub use iced_widget::*;
use crate::core::Font;
use crate::program;
pub fn monospace<'a, Renderer>(
fragment: impl text::IntoFragment<'a>,
) -> Text<'a, Theme, Renderer>
where
Renderer: program::Renderer + 'a,
{
text(fragment).font(Font::MONOSPACE)
}

View file

@ -19,7 +19,7 @@ pub fn main() -> iced::Result {
application().run() application().run()
} }
fn application() -> Application<impl Program> { fn application() -> Application<impl Program<Message = Message>> {
iced::application(Todos::new, Todos::update, Todos::view) iced::application(Todos::new, Todos::update, Todos::view)
.subscription(Todos::subscription) .subscription(Todos::subscription)
.title(Todos::title) .title(Todos::title)

View file

@ -1,4 +1,4 @@
viewport: 512x768 viewport: 500x800
mode: Impatient mode: Impatient
preset: Empty preset: Empty
----- -----

View file

@ -14,6 +14,7 @@ rust-version.workspace = true
workspace = true workspace = true
[features] [features]
debug = []
time-travel = [] time-travel = []
[dependencies] [dependencies]

View file

@ -4,6 +4,8 @@ pub use iced_runtime as runtime;
pub use iced_runtime::core; pub use iced_runtime::core;
pub use iced_runtime::futures; pub use iced_runtime::futures;
pub mod message;
mod preset; mod preset;
pub use preset::Preset; pub use preset::Preset;
@ -13,7 +15,7 @@ use crate::core::text;
use crate::core::theme; use crate::core::theme;
use crate::core::window; use crate::core::window;
use crate::core::{Element, Font, Settings}; use crate::core::{Element, Font, Settings};
use crate::futures::{Executor, Subscription}; use crate::futures::{Executor, MaybeSend, Subscription};
use crate::graphics::compositor; use crate::graphics::compositor;
use crate::runtime::Task; use crate::runtime::Task;
@ -27,7 +29,7 @@ pub trait Program: Sized {
type State; type State;
/// The message of the program. /// The message of the program.
type Message: Message + 'static; type Message: MaybeSend + 'static;
/// The theme of the program. /// The theme of the program.
type Theme: Default + theme::Base; type Theme: Default + theme::Base;
@ -43,6 +45,8 @@ pub trait Program: Sized {
fn settings(&self) -> Settings; fn settings(&self) -> Settings;
fn window(&self) -> Option<window::Settings>;
fn boot(&self) -> (Self::State, Task<Self::Message>); fn boot(&self) -> (Self::State, Task<Self::Message>);
fn update( fn update(
@ -143,6 +147,10 @@ pub fn with_title<P: Program>(
self.program.settings() self.program.settings()
} }
fn window(&self) -> Option<window::Settings> {
self.program.window()
}
fn boot(&self) -> (Self::State, Task<Self::Message>) { fn boot(&self) -> (Self::State, Task<Self::Message>) {
self.program.boot() self.program.boot()
} }
@ -229,6 +237,10 @@ pub fn with_subscription<P: Program>(
self.program.settings() self.program.settings()
} }
fn window(&self) -> Option<window::Settings> {
self.program.window()
}
fn boot(&self) -> (Self::State, Task<Self::Message>) { fn boot(&self) -> (Self::State, Task<Self::Message>) {
self.program.boot() self.program.boot()
} }
@ -316,6 +328,10 @@ pub fn with_theme<P: Program>(
self.program.settings() self.program.settings()
} }
fn window(&self) -> Option<window::Settings> {
self.program.window()
}
fn boot(&self) -> (Self::State, Task<Self::Message>) { fn boot(&self) -> (Self::State, Task<Self::Message>) {
self.program.boot() self.program.boot()
} }
@ -399,6 +415,10 @@ pub fn with_style<P: Program>(
self.program.settings() self.program.settings()
} }
fn window(&self) -> Option<window::Settings> {
self.program.window()
}
fn boot(&self) -> (Self::State, Task<Self::Message>) { fn boot(&self) -> (Self::State, Task<Self::Message>) {
self.program.boot() self.program.boot()
} }
@ -478,6 +498,10 @@ pub fn with_scale_factor<P: Program>(
self.program.settings() self.program.settings()
} }
fn window(&self) -> Option<window::Settings> {
self.program.window()
}
fn boot(&self) -> (Self::State, Task<Self::Message>) { fn boot(&self) -> (Self::State, Task<Self::Message>) {
self.program.boot() self.program.boot()
} }
@ -565,6 +589,10 @@ pub fn with_executor<P: Program, E: Executor>(
self.program.settings() self.program.settings()
} }
fn window(&self) -> Option<window::Settings> {
self.program.window()
}
fn boot(&self) -> (Self::State, Task<Self::Message>) { fn boot(&self) -> (Self::State, Task<Self::Message>) {
self.program.boot() self.program.boot()
} }
@ -683,17 +711,3 @@ impl<P: Program> Instance<P> {
self.program.scale_factor(&self.state, window) self.program.scale_factor(&self.state, window)
} }
} }
/// A trait alias for the [`Message`](Program::Message) of a [`Program`].
#[cfg(feature = "time-travel")]
pub trait Message: Send + std::fmt::Debug + Clone {}
#[cfg(feature = "time-travel")]
impl<T: Send + std::fmt::Debug + Clone> Message for T {}
/// A trait alias for the [`Message`](Program::Message) of a [`Program`].
#[cfg(not(feature = "time-travel"))]
pub trait Message: Send + std::fmt::Debug {}
#[cfg(not(feature = "time-travel"))]
impl<T: Send + std::fmt::Debug> Message for T {}

33
program/src/message.rs Normal file
View file

@ -0,0 +1,33 @@
//! Traits for the message type of a [`Program`](crate::Program).
/// A trait alias for [`Clone`], but only when the `time-travel`
/// feature is enabled.
#[cfg(feature = "time-travel")]
pub trait MaybeClone: Clone {}
#[cfg(feature = "time-travel")]
impl<T> MaybeClone for T where T: Clone {}
/// A trait alias for [`Clone`], but only when the `time-travel`
/// feature is enabled.
#[cfg(not(feature = "time-travel"))]
pub trait MaybeClone {}
#[cfg(not(feature = "time-travel"))]
impl<T> MaybeClone for T {}
/// A trait alias for [`Debug`](std::fmt::Debug), but only when the
/// `debug` feature is enabled.
#[cfg(feature = "debug")]
pub trait MaybeDebug: std::fmt::Debug {}
#[cfg(feature = "debug")]
impl<T> MaybeDebug for T where T: std::fmt::Debug {}
/// A trait alias for [`Debug`](std::fmt::Debug), but only when the
/// `debug` feature is enabled.
#[cfg(not(feature = "debug"))]
pub trait MaybeDebug {}
#[cfg(not(feature = "debug"))]
impl<T> MaybeDebug for T {}

View file

@ -9,6 +9,7 @@ use crate::futures::{BoxStream, MaybeSend, boxed_stream};
use std::convert::Infallible; use std::convert::Infallible;
use std::sync::Arc; use std::sync::Arc;
use std::thread;
#[cfg(feature = "sipper")] #[cfg(feature = "sipper")]
#[doc(no_inline)] #[doc(no_inline)]
@ -466,3 +467,47 @@ pub fn effect<T>(action: impl Into<Action<Infallible>>) -> Task<T> {
pub fn into_stream<T>(task: Task<T>) -> Option<BoxStream<Action<T>>> { pub fn into_stream<T>(task: Task<T>) -> Option<BoxStream<Action<T>>> {
task.stream task.stream
} }
/// Creates a new [`Task`] that will run the given closure in a new thread.
///
/// Any data sent by the closure through the [`mpsc::Sender`] will be produced
/// by the [`Task`].
pub fn blocking<T>(f: impl FnOnce(mpsc::Sender<T>) + Send + 'static) -> Task<T>
where
T: Send + 'static,
{
let (sender, receiver) = mpsc::channel(1);
let _ = thread::spawn(move || {
f(sender);
});
Task::stream(receiver)
}
/// Creates a new [`Task`] that will run the given closure that can fail in a new
/// thread.
///
/// Any data sent by the closure through the [`mpsc::Sender`] will be produced
/// by the [`Task`].
pub fn try_blocking<T, E>(
f: impl FnOnce(mpsc::Sender<T>) -> Result<(), E> + Send + 'static,
) -> Task<Result<T, E>>
where
T: Send + 'static,
E: Send + 'static,
{
let (sender, receiver) = mpsc::channel(1);
let (error_sender, error_receiver) = oneshot::channel();
let _ = thread::spawn(move || {
if let Err(error) = f(sender) {
let _ = error_sender.send(Err(error));
}
});
Task::stream(stream::select(
receiver.map(Ok),
stream::once(error_receiver).filter_map(async |result| result.ok()),
))
}

View file

@ -30,12 +30,14 @@
//! ] //! ]
//! } //! }
//! ``` //! ```
use crate::message;
use crate::program::{self, Program}; use crate::program::{self, Program};
use crate::shell; use crate::shell;
use crate::theme; use crate::theme;
use crate::window; use crate::window;
use crate::{ use crate::{
Element, Executor, Font, Preset, Result, Settings, Size, Subscription, Task, Element, Executor, Font, MaybeSend, Preset, Result, Settings, Size,
Subscription, Task,
}; };
use iced_debug as debug; use iced_debug as debug;
@ -81,7 +83,7 @@ pub fn application<State, Message, Theme, Renderer>(
) -> Application<impl Program<State = State, Message = Message, Theme = Theme>> ) -> Application<impl Program<State = State, Message = Message, Theme = Theme>>
where where
State: 'static, State: 'static,
Message: program::Message + 'static, Message: MaybeSend + 'static,
Theme: Default + theme::Base, Theme: Default + theme::Base,
Renderer: program::Renderer, Renderer: program::Renderer,
{ {
@ -100,7 +102,7 @@ where
impl<State, Message, Theme, Renderer, Boot, Update, View> Program impl<State, Message, Theme, Renderer, Boot, Update, View> Program
for Instance<State, Message, Theme, Renderer, Boot, Update, View> for Instance<State, Message, Theme, Renderer, Boot, Update, View>
where where
Message: program::Message + 'static, Message: MaybeSend + 'static,
Theme: Default + theme::Base, Theme: Default + theme::Base,
Renderer: program::Renderer, Renderer: program::Renderer,
Boot: self::Boot<State, Message>, Boot: self::Boot<State, Message>,
@ -142,6 +144,10 @@ where
fn settings(&self) -> Settings { fn settings(&self) -> Settings {
Settings::default() Settings::default()
} }
fn window(&self) -> Option<iced_core::window::Settings> {
Some(window::Settings::default())
}
} }
Application { Application {
@ -180,25 +186,25 @@ impl<P: Program> Application<P> {
pub fn run(self) -> Result pub fn run(self) -> Result
where where
Self: 'static, Self: 'static,
P::Message: message::MaybeDebug + message::MaybeClone,
{ {
let settings = self.settings.clone(); #[cfg(feature = "debug")]
let window = self.window.clone(); iced_debug::init(iced_debug::Metadata {
name: P::name(),
theme: None,
can_time_travel: cfg!(feature = "time-travel"),
});
#[cfg(all(feature = "debug", not(target_arch = "wasm32")))] #[cfg(feature = "tester")]
let program = { let program = iced_tester::attach(self);
iced_debug::init(iced_debug::Metadata {
name: P::name(),
theme: None,
can_time_travel: cfg!(feature = "time-travel"),
});
iced_devtools::attach(self) #[cfg(all(feature = "debug", not(feature = "tester")))]
}; let program = iced_devtools::attach(self);
#[cfg(any(not(feature = "debug"), target_arch = "wasm32"))] #[cfg(any(not(feature = "debug"), target_arch = "wasm32"))]
let program = self; let program = self;
Ok(shell::run(program, settings, Some(window))?) Ok(shell::run(program)?)
} }
/// Sets the [`Settings`] that will be used to run the [`Application`]. /// Sets the [`Settings`] that will be used to run the [`Application`].
@ -456,6 +462,10 @@ impl<P: Program> Program for Application<P> {
self.settings.clone() self.settings.clone()
} }
fn window(&self) -> Option<window::Settings> {
Some(self.window.clone())
}
fn boot(&self) -> (Self::State, Task<Self::Message>) { fn boot(&self) -> (Self::State, Task<Self::Message>) {
self.raw.boot() self.raw.boot()
} }

View file

@ -29,7 +29,7 @@ pub fn timed<State, Message, Theme, Renderer>(
> >
where where
State: 'static, State: 'static,
Message: program::Message + 'static, Message: Send + 'static,
Theme: Default + theme::Base + 'static, Theme: Default + theme::Base + 'static,
Renderer: program::Renderer + 'static, Renderer: program::Renderer + 'static,
{ {
@ -68,7 +68,7 @@ where
View, View,
> >
where where
Message: program::Message + 'static, Message: Send + 'static,
Theme: Default + theme::Base + 'static, Theme: Default + theme::Base + 'static,
Renderer: program::Renderer + 'static, Renderer: program::Renderer + 'static,
Boot: self::Boot<State, Message>, Boot: self::Boot<State, Message>,
@ -92,6 +92,10 @@ where
Settings::default() Settings::default()
} }
fn window(&self) -> Option<iced_core::window::Settings> {
Some(window::Settings::default())
}
fn boot(&self) -> (State, Task<Self::Message>) { fn boot(&self) -> (State, Task<Self::Message>) {
let (state, task) = self.boot.boot(); let (state, task) = self.boot.boot();

View file

@ -1,11 +1,13 @@
//! Create and run daemons that run in the background. //! Create and run daemons that run in the background.
use crate::application; use crate::application;
use crate::message;
use crate::program::{self, Program}; use crate::program::{self, Program};
use crate::shell; use crate::shell;
use crate::theme; use crate::theme;
use crate::window; use crate::window;
use crate::{ use crate::{
Element, Executor, Font, Preset, Result, Settings, Subscription, Task, Element, Executor, Font, MaybeSend, Preset, Result, Settings, Subscription,
Task,
}; };
use iced_debug as debug; use iced_debug as debug;
@ -29,7 +31,7 @@ pub fn daemon<State, Message, Theme, Renderer>(
) -> Daemon<impl Program<State = State, Message = Message, Theme = Theme>> ) -> Daemon<impl Program<State = State, Message = Message, Theme = Theme>>
where where
State: 'static, State: 'static,
Message: program::Message + 'static, Message: MaybeSend + 'static,
Theme: Default + theme::Base, Theme: Default + theme::Base,
Renderer: program::Renderer, Renderer: program::Renderer,
{ {
@ -48,7 +50,7 @@ where
impl<State, Message, Theme, Renderer, Boot, Update, View> Program impl<State, Message, Theme, Renderer, Boot, Update, View> Program
for Instance<State, Message, Theme, Renderer, Boot, Update, View> for Instance<State, Message, Theme, Renderer, Boot, Update, View>
where where
Message: program::Message + 'static, Message: MaybeSend + 'static,
Theme: Default + theme::Base, Theme: Default + theme::Base,
Renderer: program::Renderer, Renderer: program::Renderer,
Boot: application::Boot<State, Message>, Boot: application::Boot<State, Message>,
@ -71,6 +73,10 @@ where
Settings::default() Settings::default()
} }
fn window(&self) -> Option<iced_core::window::Settings> {
None
}
fn boot(&self) -> (Self::State, Task<Self::Message>) { fn boot(&self) -> (Self::State, Task<Self::Message>) {
self.boot.boot() self.boot.boot()
} }
@ -126,9 +132,8 @@ impl<P: Program> Daemon<P> {
pub fn run(self) -> Result pub fn run(self) -> Result
where where
Self: 'static, Self: 'static,
P::Message: message::MaybeDebug + message::MaybeClone,
{ {
let settings = self.settings.clone();
#[cfg(all(feature = "debug", not(target_arch = "wasm32")))] #[cfg(all(feature = "debug", not(target_arch = "wasm32")))]
let program = { let program = {
iced_debug::init(iced_debug::Metadata { iced_debug::init(iced_debug::Metadata {
@ -143,7 +148,7 @@ impl<P: Program> Daemon<P> {
#[cfg(any(not(feature = "debug"), target_arch = "wasm32"))] #[cfg(any(not(feature = "debug"), target_arch = "wasm32"))]
let program = self; let program = self;
Ok(shell::run(program, settings, None)?) Ok(shell::run(program)?)
} }
/// Sets the [`Settings`] that will be used to run the [`Daemon`]. /// Sets the [`Settings`] that will be used to run the [`Daemon`].
@ -298,6 +303,10 @@ impl<P: Program> Program for Daemon<P> {
self.settings.clone() self.settings.clone()
} }
fn window(&self) -> Option<window::Settings> {
None
}
fn boot(&self) -> (Self::State, Task<Self::Message>) { fn boot(&self) -> (Self::State, Task<Self::Message>) {
self.raw.boot() self.raw.boot()
} }

View file

@ -526,7 +526,9 @@ pub use crate::core::{
Rotation, Settings, Shadow, Size, Theme, Transformation, Vector, never, Rotation, Settings, Shadow, Size, Theme, Transformation, Vector, never,
}; };
pub use crate::program::Preset; pub use crate::program::Preset;
pub use crate::program::message;
pub use crate::runtime::exit; pub use crate::runtime::exit;
pub use crate::runtime::futures::MaybeSend;
pub use iced_futures::Subscription; pub use iced_futures::Subscription;
pub use Alignment::Center; pub use Alignment::Center;
@ -696,7 +698,7 @@ pub fn run<State, Message, Theme, Renderer>(
) -> Result ) -> Result
where where
State: Default + 'static, State: Default + 'static,
Message: program::Message + 'static, Message: MaybeSend + message::MaybeDebug + message::MaybeClone + 'static,
Theme: Default + theme::Base + 'static, Theme: Default + theme::Base + 'static,
Renderer: program::Renderer + 'static, Renderer: program::Renderer + 'static,
{ {

View file

@ -84,10 +84,10 @@
//! //!
//! [the classical counter interface]: https://book.iced.rs/architecture.html#dissecting-an-interface //! [the classical counter interface]: https://book.iced.rs/architecture.html#dissecting-an-interface
#![allow(missing_docs)] #![allow(missing_docs)]
use iced_program as program; pub use iced_program as program;
use iced_renderer as renderer; pub use iced_renderer as renderer;
use iced_runtime as runtime; pub use iced_runtime as runtime;
use iced_runtime::core; pub use iced_runtime::core;
pub use iced_selector as selector; pub use iced_selector as selector;

20
tester/Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "iced_tester"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
categories.workspace = true
keywords.workspace = true
rust-version.workspace = true
[dependencies]
iced_test.workspace = true
iced_widget.workspace = true
log.workspace = true
rfd.workspace = true
[lints]
workspace = true

View file

@ -3,7 +3,7 @@ use crate::core::Font;
use crate::program; use crate::program;
use crate::widget::{Text, text}; use crate::widget::{Text, text};
pub const FONT: &[u8] = include_bytes!("../fonts/iced_devtools-icons.ttf"); pub const FONT: &[u8] = include_bytes!("../fonts/iced_tester-icons.ttf");
pub fn cancel<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> pub fn cancel<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer>
where where

View file

@ -1,29 +1,97 @@
//! Record, edit, and run end-to-end tests for your iced applications.
#![allow(missing_docs)]
pub use iced_test as test;
pub use iced_test::core;
pub use iced_test::program;
pub use iced_test::runtime;
pub use iced_test::runtime::futures;
pub use iced_widget as widget;
mod icon;
mod recorder; mod recorder;
use recorder::recorder; use recorder::recorder;
use crate::Program;
use crate::core::Alignment::Center; use crate::core::Alignment::Center;
use crate::core::Length::Fill; use crate::core::Length::Fill;
use crate::core::alignment::Horizontal::Right; use crate::core::alignment::Horizontal::Right;
use crate::core::border; use crate::core::border;
use crate::core::mouse; use crate::core::mouse;
use crate::core::window; use crate::core::window;
use crate::core::{Element, Font, Size, Theme}; use crate::core::{Element, Font, Settings, Size, Theme};
use crate::executor;
use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::mpsc;
use crate::icon; use crate::program::Program;
use crate::program; use crate::runtime::task::{self, Task};
use crate::runtime::Task;
use crate::test::emulator; use crate::test::emulator;
use crate::test::ice; use crate::test::ice;
use crate::test::instruction; use crate::test::instruction;
use crate::test::{Emulator, Ice, Instruction}; use crate::test::{Emulator, Ice, Instruction};
use crate::widget::{ use crate::widget::{
button, center, column, combo_box, container, horizontal_space, monospace, button, center, column, combo_box, container, horizontal_space, pick_list,
pick_list, row, scrollable, text, text_editor, text_input, themer, row, scrollable, text, text_editor, text_input, themer,
}; };
/// Attaches a [`Tester`] to the given [`Program`].
pub fn attach<P: Program + 'static>(program: P) -> Attach<P> {
Attach { program }
}
/// A [`Program`] with a [`Tester`] attached to it.
#[derive(Debug)]
pub struct Attach<P> {
/// The original [`Program`] attatched to the [`Tester`].
pub program: P,
}
impl<P> Program for Attach<P>
where
P: Program + 'static,
{
type State = Tester<P>;
type Message = Tick<P>;
type Theme = Theme;
type Renderer = P::Renderer;
type Executor = P::Executor;
fn name() -> &'static str {
P::name()
}
fn settings(&self) -> Settings {
let mut settings = self.program.settings();
settings.fonts.push(icon::FONT.into());
settings
}
fn window(&self) -> Option<window::Settings> {
self.program.window().map(|window| window::Settings {
size: window.size + Size::new(300.0, 80.0),
..window
})
}
fn boot(&self) -> (Self::State, Task<Self::Message>) {
(Tester::new(&self.program), Task::none())
}
fn update(
&self,
state: &mut Self::State,
message: Self::Message,
) -> Task<Self::Message> {
state.tick(&self.program, message)
}
fn view<'a>(
&self,
state: &'a Self::State,
window: window::Id,
) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
state.view(&self.program, window)
}
}
#[allow(missing_debug_implementations)]
pub struct Tester<P: Program> { pub struct Tester<P: Program> {
viewport: Size, viewport: Size,
mode: emulator::Mode, mode: emulator::Mode,
@ -35,7 +103,10 @@ pub struct Tester<P: Program> {
} }
enum State<P: Program> { enum State<P: Program> {
Idle, Empty,
Idle {
state: P::State,
},
Recording { Recording {
emulator: Emulator<P>, emulator: Emulator<P>,
}, },
@ -84,9 +155,12 @@ pub enum Tick<P: Program> {
impl<P: Program + 'static> Tester<P> { impl<P: Program + 'static> Tester<P> {
pub fn new(program: &P) -> Self { pub fn new(program: &P) -> Self {
let (state, _) = program.boot();
let window = program.window().unwrap_or_default();
Self { Self {
mode: emulator::Mode::default(), mode: emulator::Mode::default(),
viewport: window::Settings::default().size, viewport: window.size,
presets: combo_box::State::new( presets: combo_box::State::new(
program program
.presets() .presets()
@ -97,7 +171,7 @@ impl<P: Program + 'static> Tester<P> {
), ),
preset: None, preset: None,
instructions: Vec::new(), instructions: Vec::new(),
state: State::Idle, state: State::Idle { state },
edit: None, edit: None,
} }
} }
@ -150,7 +224,7 @@ impl<P: Program + 'static> Tester<P> {
} }
Message::Stop => { Message::Stop => {
let State::Recording { emulator } = let State::Recording { emulator } =
std::mem::replace(&mut self.state, State::Idle) std::mem::replace(&mut self.state, State::Empty)
else { else {
return Task::none(); return Task::none();
}; };
@ -204,7 +278,7 @@ impl<P: Program + 'static> Tester<P> {
Task::future(import) Task::future(import)
.and_then(|file| { .and_then(|file| {
executor::spawn_blocking(move |mut sender| { task::blocking(move |mut sender| {
let _ = sender.try_send(Ice::parse( let _ = sender.try_send(Ice::parse(
&fs::read_to_string(file.path()) &fs::read_to_string(file.path())
.unwrap_or_default(), .unwrap_or_default(),
@ -248,7 +322,13 @@ impl<P: Program + 'static> Tester<P> {
self.preset = ice.preset; self.preset = ice.preset;
self.instructions = ice.instructions; self.instructions = ice.instructions;
self.edit = None; self.edit = None;
self.state = State::Idle;
let (state, _) = self
.preset(program)
.map(program::Preset::boot)
.unwrap_or_else(|| program.boot());
self.state = State::Idle { state };
Task::none() Task::none()
} }
@ -358,7 +438,9 @@ impl<P: Program + 'static> Tester<P> {
} }
} }
}, },
State::Idle | State::Asserting { .. } => {} State::Empty
| State::Idle { .. }
| State::Asserting { .. } => {}
} }
Task::none() Task::none()
@ -432,15 +514,14 @@ impl<P: Program + 'static> Tester<P> {
} }
} }
pub fn view<'a, T: 'static>( pub fn view<'a>(
&'a self, &'a self,
program: &P, program: &P,
current: impl FnOnce() -> Element<'a, T, Theme, P::Renderer>, window: window::Id,
emulate: impl Fn(Tick<P>) -> T + 'a, ) -> Element<'a, Tick<P>, Theme, P::Renderer> {
) -> Element<'a, T, Theme, P::Renderer> {
let status = { let status = {
let (icon, label) = match &self.state { let (icon, label) = match &self.state {
State::Idle => (text(""), "Idle"), State::Empty | State::Idle { .. } => (text(""), "Idle"),
State::Recording { .. } => (icon::record(), "Recording"), State::Recording { .. } => (icon::record(), "Recording"),
State::Asserting { .. } => (icon::lightbulb(), "Asserting"), State::Asserting { .. } => (icon::lightbulb(), "Asserting"),
State::Playing { outcome, .. } => match outcome { State::Playing { outcome, .. } => match outcome {
@ -456,7 +537,9 @@ impl<P: Program + 'static> Tester<P> {
container::Style { container::Style {
text_color: Some(match &self.state { text_color: Some(match &self.state {
State::Idle => palette.background.strongest.color, State::Empty | State::Idle { .. } => {
palette.background.strongest.color
}
State::Recording { .. } => { State::Recording { .. } => {
palette.danger.base.color palette.danger.base.color
} }
@ -483,33 +566,34 @@ impl<P: Program + 'static> Tester<P> {
let viewport = container( let viewport = container(
scrollable( scrollable(
container(match &self.state { container(match &self.state {
State::Idle => current(), State::Empty => horizontal_space().into(),
State::Idle { state } => Element::from(themer(
program.theme(state, window),
program.view(state, window),
))
.map(Tick::Program),
State::Recording { emulator } => { State::Recording { emulator } => {
let theme = emulator.theme(program); let theme = emulator.theme(program);
let view = emulator.view(program).map(Tick::Program); let view = emulator.view(program).map(Tick::Program);
Element::from( recorder(themer(theme, view))
recorder(themer(theme, view)) .on_record(Tick::Record)
.on_record(Tick::Record), .into()
)
.map(emulate)
} }
State::Asserting { state, window, .. } => { State::Asserting { state, window, .. } => {
let theme = program.theme(state, *window); let theme = program.theme(state, *window);
let view = let view =
program.view(state, *window).map(Tick::Program); program.view(state, *window).map(Tick::Program);
Element::from( recorder(themer(theme, view))
recorder(themer(theme, view)) .on_record(Tick::Assert)
.on_record(Tick::Assert), .into()
)
.map(emulate)
} }
State::Playing { emulator, .. } => { State::Playing { emulator, .. } => {
let theme = emulator.theme(program); let theme = emulator.theme(program);
let view = emulator.view(program).map(Tick::Program); let view = emulator.view(program).map(Tick::Program);
Element::from(themer(theme, view)).map(emulate) themer(theme, view).into()
} }
}) })
.width(self.viewport.width) .width(self.viewport.width)
@ -525,7 +609,9 @@ impl<P: Program + 'static> Tester<P> {
container::Style { container::Style {
border: border::width(2.0).color(match &self.state { border: border::width(2.0).color(match &self.state {
State::Idle => palette.background.strongest.color, State::Empty | State::Idle { .. } => {
palette.background.strongest.color
}
State::Recording { .. } => palette.danger.base.color, State::Recording { .. } => palette.danger.base.color,
State::Asserting { .. } => palette.warning.weak.color, State::Asserting { .. } => palette.warning.weak.color,
State::Playing { outcome, .. } => match outcome { State::Playing { outcome, .. } => match outcome {
@ -539,9 +625,15 @@ impl<P: Program + 'static> Tester<P> {
}) })
.padding(10); .padding(10);
center(column![status, viewport].spacing(10).align_x(Right)) row![
.padding(10) center(column![status, viewport].spacing(10).align_x(Right))
.into() .padding(10),
container(self.controls().map(Tick::Tester))
.width(250)
.padding(10)
.style(container::dark)
]
.into()
} }
pub fn controls(&self) -> Element<'_, Message, Theme, P::Renderer> { pub fn controls(&self) -> Element<'_, Message, Theme, P::Renderer> {
@ -552,7 +644,7 @@ impl<P: Program + 'static> Tester<P> {
width: width.parse().unwrap_or(self.viewport.width), width: width.parse().unwrap_or(self.viewport.width),
..self.viewport ..self.viewport
})), })),
monospace("x").size(14), text("x").size(14).font(Font::MONOSPACE),
text_input("Height", &self.viewport.height.to_string()) text_input("Height", &self.viewport.height.to_string())
.size(14) .size(14)
.on_input(|height| Message::ChangeViewport(Size { .on_input(|height| Message::ChangeViewport(Size {
@ -590,8 +682,9 @@ impl<P: Program + 'static> Tester<P> {
.into() .into()
} else if self.instructions.is_empty() { } else if self.instructions.is_empty() {
Element::from(center( Element::from(center(
monospace("No instructions recorded yet!") text("No instructions recorded yet!")
.size(14) .size(14)
.font(Font::MONOSPACE)
.width(Fill) .width(Fill)
.center(), .center(),
)) ))
@ -599,9 +692,10 @@ impl<P: Program + 'static> Tester<P> {
scrollable( scrollable(
column(self.instructions.iter().enumerate().map( column(self.instructions.iter().enumerate().map(
|(i, instruction)| { |(i, instruction)| {
monospace(instruction.to_string()) text(instruction.to_string())
.wrapping(text::Wrapping::None) // TODO: Ellipsize? .wrapping(text::Wrapping::None) // TODO: Ellipsize?
.size(10) .size(10)
.font(Font::MONOSPACE)
.style(move |theme: &Theme| text::Style { .style(move |theme: &Theme| text::Style {
color: match &self.state { color: match &self.state {
State::Playing { State::Playing {
@ -730,9 +824,12 @@ where
Message: 'a, Message: 'a,
Renderer: program::Renderer + 'a, Renderer: program::Renderer + 'a,
{ {
column![monospace(fragment).size(14), content.into()] column![
.spacing(5) text(fragment).size(14).font(Font::MONOSPACE),
.into() content.into()
]
.spacing(5)
.into()
} }
fn labeled_with<'a, Message, Renderer>( fn labeled_with<'a, Message, Renderer>(
@ -746,7 +843,7 @@ where
{ {
column![ column![
row![ row![
monospace(fragment).size(14), text(fragment).size(14).font(Font::MONOSPACE),
horizontal_space(), horizontal_space(),
control.into() control.into()
] ]

View file

@ -45,7 +45,7 @@ use crate::core::renderer;
use crate::core::theme; use crate::core::theme;
use crate::core::time::Instant; use crate::core::time::Instant;
use crate::core::widget::operation; use crate::core::widget::operation;
use crate::core::{Point, Settings, Size}; use crate::core::{Point, Size};
use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::mpsc;
use crate::futures::futures::channel::oneshot; use crate::futures::futures::channel::oneshot;
use crate::futures::futures::task; use crate::futures::futures::task;
@ -66,11 +66,7 @@ use std::slice;
use std::sync::Arc; use std::sync::Arc;
/// Runs a [`Program`] with the provided settings. /// Runs a [`Program`] with the provided settings.
pub fn run<P>( pub fn run<P>(program: P) -> Result<(), Error>
program: P,
settings: Settings,
window_settings: Option<window::Settings>,
) -> Result<(), Error>
where where
P: Program + 'static, P: Program + 'static,
P::Theme: theme::Base, P::Theme: theme::Base,
@ -78,6 +74,8 @@ where
use winit::event_loop::EventLoop; use winit::event_loop::EventLoop;
let boot_span = debug::boot(); let boot_span = debug::boot();
let settings = program.settings();
let window_settings = program.window();
let graphics_settings = settings.clone().into(); let graphics_settings = settings.clone().into();
let event_loop = EventLoop::with_user_event() let event_loop = EventLoop::with_user_event()
@ -169,7 +167,6 @@ where
impl<Message, F> winit::application::ApplicationHandler<Action<Message>> impl<Message, F> winit::application::ApplicationHandler<Action<Message>>
for Runner<Message, F> for Runner<Message, F>
where where
Message: std::fmt::Debug,
F: Future<Output = ()>, F: Future<Output = ()>,
{ {
fn resumed( fn resumed(

View file

@ -88,10 +88,7 @@ impl<T: 'static> Proxy<T> {
/// ///
/// Note: This skips the backpressure mechanism with an unbounded /// Note: This skips the backpressure mechanism with an unbounded
/// channel. Use sparingly! /// channel. Use sparingly!
pub fn send_action(&self, action: Action<T>) pub fn send_action(&self, action: Action<T>) {
where
T: std::fmt::Debug,
{
let _ = self.raw.send_event(action); let _ = self.raw.send_event(action);
} }