Merge pull request #2910 from iced-rs/feature/time-travel
Time Travel Debugging
This commit is contained in:
commit
8ba993adad
36 changed files with 1174 additions and 356 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -2453,6 +2453,7 @@ version = "0.14.0-dev"
|
|||
dependencies = [
|
||||
"iced_beacon",
|
||||
"iced_core",
|
||||
"iced_futures",
|
||||
"log",
|
||||
]
|
||||
|
||||
|
|
@ -2463,6 +2464,7 @@ dependencies = [
|
|||
"iced_debug",
|
||||
"iced_program",
|
||||
"iced_widget",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ markdown = ["iced_widget/markdown"]
|
|||
lazy = ["iced_widget/lazy"]
|
||||
# Enables a debug view in native platforms (press F12)
|
||||
debug = ["iced_winit/debug", "iced_devtools"]
|
||||
# Enables time-travel debugging (very experimental!)
|
||||
time-travel = ["debug", "iced_devtools/time-travel"]
|
||||
# Enables the `thread-pool` futures executor as the `executor::Default` on native platforms
|
||||
thread-pool = ["iced_futures/thread-pool"]
|
||||
# Enables `tokio` as the `executor::Default` on native platforms
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
use crate::Error;
|
||||
use crate::core::time::{Duration, SystemTime};
|
||||
use crate::span;
|
||||
use crate::theme;
|
||||
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{self, AsyncWriteExt};
|
||||
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
use tokio::task;
|
||||
use tokio::time;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
|
@ -17,7 +19,7 @@ pub const SERVER_ADDRESS: &str = "127.0.0.1:9167";
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
sender: mpsc::Sender<Message>,
|
||||
sender: mpsc::Sender<Action>,
|
||||
is_connected: Arc<AtomicBool>,
|
||||
_handle: Arc<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
|
@ -43,17 +45,17 @@ pub enum Event {
|
|||
ThemeChanged(theme::Palette),
|
||||
SpanStarted(span::Stage),
|
||||
SpanFinished(span::Stage, Duration),
|
||||
MessageLogged(String),
|
||||
MessageLogged { number: usize, message: String },
|
||||
CommandsSpawned(usize),
|
||||
SubscriptionsTracked(usize),
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn log(&self, event: Event) {
|
||||
let _ = self.sender.try_send(Message::EventLogged {
|
||||
let _ = self.sender.try_send(Action::Send(Message::EventLogged {
|
||||
at: SystemTime::now(),
|
||||
event,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn is_connected(&self) -> bool {
|
||||
|
|
@ -61,21 +63,28 @@ impl Client {
|
|||
}
|
||||
|
||||
pub fn quit(&self) {
|
||||
let _ = self.sender.try_send(Message::Quit {
|
||||
let _ = self.sender.try_send(Action::Send(Message::Quit {
|
||||
at: SystemTime::now(),
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> mpsc::Receiver<Command> {
|
||||
let (sender, receiver) = mpsc::channel(100);
|
||||
let _ = self.sender.try_send(Action::Forward(sender));
|
||||
|
||||
receiver
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn connect(name: String) -> Client {
|
||||
let (sender, receiver) = mpsc::channel(100);
|
||||
let (sender, receiver) = mpsc::channel(10_000);
|
||||
let is_connected = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let handle = {
|
||||
let is_connected = is_connected.clone();
|
||||
|
||||
std::thread::spawn(move || run(name, is_connected.clone(), receiver))
|
||||
std::thread::spawn(move || run(name, is_connected, receiver))
|
||||
};
|
||||
|
||||
Client {
|
||||
|
|
@ -85,22 +94,42 @@ pub fn connect(name: String) -> Client {
|
|||
}
|
||||
}
|
||||
|
||||
enum Action {
|
||||
Send(Message),
|
||||
Forward(mpsc::Sender<Command>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum Command {
|
||||
RewindTo { message: usize },
|
||||
GoLive,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn run(
|
||||
name: String,
|
||||
is_connected: Arc<AtomicBool>,
|
||||
mut receiver: mpsc::Receiver<Message>,
|
||||
mut receiver: mpsc::Receiver<Action>,
|
||||
) {
|
||||
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
|
||||
.expect("Parse package version");
|
||||
|
||||
let command_sender = {
|
||||
// Discard by default
|
||||
let (sender, _receiver) = mpsc::channel(1);
|
||||
|
||||
Arc::new(Mutex::new(sender))
|
||||
};
|
||||
|
||||
loop {
|
||||
match _connect().await {
|
||||
Ok(mut stream) => {
|
||||
Ok(stream) => {
|
||||
is_connected.store(true, atomic::Ordering::Relaxed);
|
||||
|
||||
let (mut reader, mut writer) = stream.into_split();
|
||||
|
||||
let _ = send(
|
||||
&mut stream,
|
||||
&mut writer,
|
||||
Message::Connected {
|
||||
at: SystemTime::now(),
|
||||
name: name.clone(),
|
||||
|
|
@ -109,16 +138,48 @@ async fn run(
|
|||
)
|
||||
.await;
|
||||
|
||||
while let Some(output) = receiver.recv().await {
|
||||
match send(&mut stream, output).await {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
if error.kind() != io::ErrorKind::BrokenPipe {
|
||||
log::warn!(
|
||||
"Error sending message to server: {error}"
|
||||
);
|
||||
{
|
||||
let command_sender = command_sender.clone();
|
||||
|
||||
drop(task::spawn(async move {
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
loop {
|
||||
match receive(&mut reader, &mut buffer).await {
|
||||
Ok(command) => {
|
||||
let sender = command_sender.lock().await;
|
||||
let _ = sender.send(command).await;
|
||||
}
|
||||
Err(Error::DecodingFailed(_)) => {}
|
||||
Err(Error::IOFailed(_)) => break,
|
||||
}
|
||||
break;
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
while let Some(action) = receiver.recv().await {
|
||||
match action {
|
||||
Action::Send(message) => {
|
||||
match send(&mut writer, message).await {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
if error.kind() != io::ErrorKind::BrokenPipe
|
||||
{
|
||||
log::warn!(
|
||||
"Error sending message to server: {error}"
|
||||
);
|
||||
}
|
||||
|
||||
is_connected.store(
|
||||
false,
|
||||
atomic::Ordering::Relaxed,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::Forward(sender) => {
|
||||
*command_sender.lock().await = sender;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,7 +203,7 @@ async fn _connect() -> Result<net::TcpStream, io::Error> {
|
|||
}
|
||||
|
||||
async fn send(
|
||||
stream: &mut net::TcpStream,
|
||||
stream: &mut net::tcp::OwnedWriteHalf,
|
||||
message: Message,
|
||||
) -> Result<(), io::Error> {
|
||||
let bytes = bincode::serialize(&message).expect("Encode input message");
|
||||
|
|
@ -154,3 +215,18 @@ async fn send(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive(
|
||||
stream: &mut net::tcp::OwnedReadHalf,
|
||||
buffer: &mut Vec<u8>,
|
||||
) -> Result<Command, Error> {
|
||||
let size = stream.read_u64().await? as usize;
|
||||
|
||||
if buffer.len() < size {
|
||||
buffer.resize(size, 0);
|
||||
}
|
||||
|
||||
let _n = stream.read_exact(&mut buffer[..size]).await?;
|
||||
|
||||
Ok(bincode::deserialize(buffer)?)
|
||||
}
|
||||
|
|
|
|||
9
beacon/src/error.rs
Normal file
9
beacon/src/error.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use std::io;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("input/output operation failed: {0}")]
|
||||
IOFailed(#[from] io::Error),
|
||||
#[error("decoding failed: {0}")]
|
||||
DecodingFailed(#[from] Box<bincode::ErrorKind>),
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ pub use semver::Version;
|
|||
pub mod client;
|
||||
pub mod span;
|
||||
|
||||
mod error;
|
||||
mod stream;
|
||||
|
||||
pub use client::Client;
|
||||
|
|
@ -11,14 +12,44 @@ pub use span::Span;
|
|||
|
||||
use crate::core::theme;
|
||||
use crate::core::time::{Duration, SystemTime};
|
||||
use crate::error::Error;
|
||||
|
||||
use futures::{SinkExt, Stream};
|
||||
use tokio::io::{self, AsyncReadExt};
|
||||
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Connection {
|
||||
commands: mpsc::Sender<client::Command>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub fn rewind_to<'a>(
|
||||
&self,
|
||||
message: usize,
|
||||
) -> impl Future<Output = ()> + 'a {
|
||||
let commands = self.commands.clone();
|
||||
|
||||
async move {
|
||||
let _ = commands.send(client::Command::RewindTo { message }).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go_live<'a>(&self) -> impl Future<Output = ()> + 'a {
|
||||
let commands = self.commands.clone();
|
||||
|
||||
async move {
|
||||
let _ = commands.send(client::Command::GoLive).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
Connected {
|
||||
connection: Connection,
|
||||
at: SystemTime,
|
||||
name: String,
|
||||
version: Version,
|
||||
|
|
@ -30,10 +61,6 @@ pub enum Event {
|
|||
at: SystemTime,
|
||||
palette: theme::Palette,
|
||||
},
|
||||
SubscriptionsTracked {
|
||||
at: SystemTime,
|
||||
amount_alive: usize,
|
||||
},
|
||||
SpanFinished {
|
||||
at: SystemTime,
|
||||
duration: Duration,
|
||||
|
|
@ -53,7 +80,6 @@ impl Event {
|
|||
Self::Connected { at, .. }
|
||||
| Self::Disconnected { at, .. }
|
||||
| Self::ThemeChanged { at, .. }
|
||||
| Self::SubscriptionsTracked { at, .. }
|
||||
| Self::SpanFinished { at, .. }
|
||||
| Self::QuitRequested { at }
|
||||
| Self::AlreadyRunning { at } => *at,
|
||||
|
|
@ -86,18 +112,48 @@ pub fn run() -> impl Stream<Item = Event> {
|
|||
};
|
||||
|
||||
loop {
|
||||
let Ok((mut stream, _)) = server.accept().await else {
|
||||
let Ok((stream, _)) = server.accept().await else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let _ = stream.set_nodelay(true);
|
||||
let (mut reader, mut writer) = {
|
||||
let _ = stream.set_nodelay(true);
|
||||
stream.into_split()
|
||||
};
|
||||
|
||||
let (command_sender, mut command_receiver) = mpsc::channel(1);
|
||||
let mut last_message = String::new();
|
||||
let mut last_commands_spawned = 0;
|
||||
let mut last_update_number = 0;
|
||||
let mut last_tasks = 0;
|
||||
let mut last_subscriptions = 0;
|
||||
let mut last_present_window = None;
|
||||
|
||||
drop(task::spawn(async move {
|
||||
let mut last_message_number = None;
|
||||
|
||||
while let Some(command) = command_receiver.recv().await {
|
||||
match command {
|
||||
client::Command::RewindTo { message } => {
|
||||
if Some(message) == last_message_number {
|
||||
continue;
|
||||
}
|
||||
|
||||
last_message_number = Some(message);
|
||||
}
|
||||
client::Command::GoLive => {
|
||||
last_message_number = None;
|
||||
}
|
||||
}
|
||||
|
||||
let _ =
|
||||
send(&mut writer, command).await.inspect_err(|error| {
|
||||
log::error!("Error when sending command: {error}")
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
loop {
|
||||
match receive(&mut stream, &mut buffer).await {
|
||||
match receive(&mut reader, &mut buffer).await {
|
||||
Ok(message) => {
|
||||
match message {
|
||||
client::Message::Connected {
|
||||
|
|
@ -107,6 +163,9 @@ pub fn run() -> impl Stream<Item = Event> {
|
|||
} => {
|
||||
let _ = output
|
||||
.send(Event::Connected {
|
||||
connection: Connection {
|
||||
commands: command_sender.clone(),
|
||||
},
|
||||
at,
|
||||
name,
|
||||
version,
|
||||
|
|
@ -126,26 +185,25 @@ pub fn run() -> impl Stream<Item = Event> {
|
|||
client::Event::SubscriptionsTracked(
|
||||
amount_alive,
|
||||
) => {
|
||||
let _ = output
|
||||
.send(Event::SubscriptionsTracked {
|
||||
at,
|
||||
amount_alive,
|
||||
})
|
||||
.await;
|
||||
last_subscriptions = amount_alive;
|
||||
}
|
||||
client::Event::MessageLogged(message) => {
|
||||
client::Event::MessageLogged {
|
||||
number,
|
||||
message,
|
||||
} => {
|
||||
last_update_number = number;
|
||||
last_message = message;
|
||||
}
|
||||
client::Event::CommandsSpawned(
|
||||
commands,
|
||||
) => {
|
||||
last_commands_spawned = commands;
|
||||
last_tasks = commands;
|
||||
}
|
||||
client::Event::SpanStarted(
|
||||
span::Stage::Update,
|
||||
) => {
|
||||
last_message.clear();
|
||||
last_commands_spawned = 0;
|
||||
last_tasks = 0;
|
||||
}
|
||||
client::Event::SpanStarted(
|
||||
span::Stage::Present(window),
|
||||
|
|
@ -161,10 +219,12 @@ pub fn run() -> impl Stream<Item = Event> {
|
|||
span::Stage::Boot => Span::Boot,
|
||||
span::Stage::Update => {
|
||||
Span::Update {
|
||||
number: last_update_number,
|
||||
message: last_message
|
||||
.clone(),
|
||||
commands_spawned:
|
||||
last_commands_spawned,
|
||||
tasks: last_tasks,
|
||||
subscriptions:
|
||||
last_subscriptions,
|
||||
}
|
||||
}
|
||||
span::Stage::View(window) => {
|
||||
|
|
@ -246,7 +306,7 @@ pub fn run() -> impl Stream<Item = Event> {
|
|||
}
|
||||
|
||||
async fn receive(
|
||||
stream: &mut net::TcpStream,
|
||||
stream: &mut net::tcp::OwnedReadHalf,
|
||||
buffer: &mut Vec<u8>,
|
||||
) -> Result<client::Message, Error> {
|
||||
let size = stream.read_u64().await? as usize;
|
||||
|
|
@ -260,14 +320,20 @@ async fn receive(
|
|||
Ok(bincode::deserialize(buffer)?)
|
||||
}
|
||||
|
||||
async fn send(
|
||||
stream: &mut net::tcp::OwnedWriteHalf,
|
||||
command: client::Command,
|
||||
) -> Result<(), io::Error> {
|
||||
let bytes = bincode::serialize(&command).expect("Encode input message");
|
||||
let size = bytes.len() as u64;
|
||||
|
||||
stream.write_all(&size.to_be_bytes()).await?;
|
||||
stream.write_all(&bytes).await?;
|
||||
stream.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delay() {
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum Error {
|
||||
#[error("input/output operation failed: {0}")]
|
||||
IOFailed(#[from] io::Error),
|
||||
#[error("decoding failed: {0}")]
|
||||
DecodingFailed(#[from] Box<bincode::ErrorKind>),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ use serde::{Deserialize, Serialize};
|
|||
pub enum Span {
|
||||
Boot,
|
||||
Update {
|
||||
number: usize,
|
||||
message: String,
|
||||
commands_spawned: usize,
|
||||
tasks: usize,
|
||||
subscriptions: usize,
|
||||
},
|
||||
View {
|
||||
window: window::Id,
|
||||
|
|
|
|||
|
|
@ -90,15 +90,17 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Transitions the [`Animation`] from its current state to the given new state.
|
||||
pub fn go(mut self, new_state: T) -> Self {
|
||||
self.go_mut(new_state);
|
||||
/// Transitions the [`Animation`] from its current state to the given new state
|
||||
/// at the given time.
|
||||
pub fn go(mut self, new_state: T, at: Instant) -> Self {
|
||||
self.go_mut(new_state, at);
|
||||
self
|
||||
}
|
||||
|
||||
/// Transitions the [`Animation`] from its current state to the given new state, by reference.
|
||||
pub fn go_mut(&mut self, new_state: T) {
|
||||
self.raw.transition(new_state, Instant::now());
|
||||
/// Transitions the [`Animation`] from its current state to the given new state
|
||||
/// at the given time, by reference.
|
||||
pub fn go_mut(&mut self, new_state: T, at: Instant) {
|
||||
self.raw.transition(new_state, at);
|
||||
}
|
||||
|
||||
/// Returns true if the [`Animation`] is currently in progress.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ impl text::Renderer for () {
|
|||
type Paragraph = ();
|
||||
type Editor = ();
|
||||
|
||||
const MONOSPACE_FONT: Font = Font::MONOSPACE;
|
||||
const ICON_FONT: Font = Font::DEFAULT;
|
||||
const CHECKMARK_ICON: char = '0';
|
||||
const ARROW_DOWN_ICON: char = '0';
|
||||
|
|
|
|||
|
|
@ -232,6 +232,11 @@ pub trait Renderer: crate::Renderer {
|
|||
/// The [`Editor`] of this [`Renderer`].
|
||||
type Editor: Editor<Font = Self::Font> + 'static;
|
||||
|
||||
/// A monospace font.
|
||||
///
|
||||
/// It may be used by devtools.
|
||||
const MONOSPACE_FONT: Self::Font;
|
||||
|
||||
/// The icon font of the backend.
|
||||
const ICON_FONT: Self::Font;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ pub use palette::Palette;
|
|||
|
||||
use crate::Color;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -87,14 +88,17 @@ impl Theme {
|
|||
];
|
||||
|
||||
/// Creates a new custom [`Theme`] from the given [`Palette`].
|
||||
pub fn custom(name: String, palette: Palette) -> Self {
|
||||
pub fn custom(
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
palette: Palette,
|
||||
) -> Self {
|
||||
Self::custom_with_fn(name, palette, palette::Extended::generate)
|
||||
}
|
||||
|
||||
/// Creates a new custom [`Theme`] from the given [`Palette`], with
|
||||
/// a custom generator of a [`palette::Extended`].
|
||||
pub fn custom_with_fn(
|
||||
name: String,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
palette: Palette,
|
||||
generate: impl FnOnce(Palette) -> palette::Extended,
|
||||
) -> Self {
|
||||
|
|
@ -220,7 +224,7 @@ impl fmt::Display for Theme {
|
|||
/// A [`Theme`] with a customized [`Palette`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Custom {
|
||||
name: String,
|
||||
name: Cow<'static, str>,
|
||||
palette: Palette,
|
||||
extended: palette::Extended,
|
||||
}
|
||||
|
|
@ -234,12 +238,12 @@ impl Custom {
|
|||
/// Creates a [`Custom`] theme from the given [`Palette`] with
|
||||
/// a custom generator of a [`palette::Extended`].
|
||||
pub fn with_fn(
|
||||
name: String,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
palette: Palette,
|
||||
generate: impl FnOnce(Palette) -> palette::Extended,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
name: name.into(),
|
||||
palette,
|
||||
extended: generate(palette),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ enable = ["dep:iced_beacon"]
|
|||
|
||||
[dependencies]
|
||||
iced_core.workspace = true
|
||||
iced_futures.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
|
|
|
|||
127
debug/src/lib.rs
127
debug/src/lib.rs
|
|
@ -1,12 +1,12 @@
|
|||
pub use iced_core as core;
|
||||
pub use iced_futures as futures;
|
||||
|
||||
use crate::core::theme;
|
||||
use crate::core::window;
|
||||
use crate::futures::Subscription;
|
||||
|
||||
pub use internal::Span;
|
||||
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Primitive {
|
||||
Quad,
|
||||
|
|
@ -16,12 +16,26 @@ pub enum Primitive {
|
|||
Text,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Command {
|
||||
RewindTo { message: usize },
|
||||
GoLive,
|
||||
}
|
||||
|
||||
pub fn enable() {
|
||||
internal::enable();
|
||||
}
|
||||
|
||||
pub fn disable() {
|
||||
internal::disable();
|
||||
}
|
||||
|
||||
pub fn init(name: &str) {
|
||||
internal::init(name);
|
||||
}
|
||||
|
||||
pub fn toggle_comet() -> Result<(), io::Error> {
|
||||
internal::toggle_comet()
|
||||
pub fn quit() -> bool {
|
||||
internal::quit()
|
||||
}
|
||||
|
||||
pub fn theme_changed(f: impl FnOnce() -> Option<theme::Palette>) {
|
||||
|
|
@ -84,25 +98,25 @@ pub fn time_with<T>(name: impl Into<String>, f: impl FnOnce() -> T) -> T {
|
|||
result
|
||||
}
|
||||
|
||||
pub fn skip_next_timing() {
|
||||
internal::skip_next_timing();
|
||||
pub fn commands() -> Subscription<Command> {
|
||||
internal::commands()
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enable", not(target_arch = "wasm32")))]
|
||||
mod internal {
|
||||
use crate::Primitive;
|
||||
use crate::core::theme;
|
||||
use crate::core::time::Instant;
|
||||
use crate::core::window;
|
||||
use crate::futures::Subscription;
|
||||
use crate::futures::futures::Stream;
|
||||
use crate::{Command, Primitive};
|
||||
|
||||
use iced_beacon as beacon;
|
||||
|
||||
use beacon::client::{self, Client};
|
||||
use beacon::span;
|
||||
|
||||
use std::io;
|
||||
use std::process;
|
||||
use std::sync::atomic::{self, AtomicBool};
|
||||
use std::sync::atomic::{self, AtomicBool, AtomicUsize};
|
||||
use std::sync::{LazyLock, RwLock};
|
||||
|
||||
pub fn init(name: &str) {
|
||||
|
|
@ -111,25 +125,13 @@ mod internal {
|
|||
name.clone_into(&mut NAME.write().expect("Write application name"));
|
||||
}
|
||||
|
||||
pub fn toggle_comet() -> Result<(), io::Error> {
|
||||
pub fn quit() -> bool {
|
||||
if BEACON.is_connected() {
|
||||
BEACON.quit();
|
||||
|
||||
Ok(())
|
||||
true
|
||||
} else {
|
||||
let _ = process::Command::new("iced_comet")
|
||||
.stdin(process::Stdio::null())
|
||||
.stdout(process::Stdio::null())
|
||||
.stderr(process::Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
if let Some(palette) =
|
||||
LAST_PALETTE.read().expect("Read last palette").as_ref()
|
||||
{
|
||||
BEACON.log(client::Event::ThemeChanged(*palette));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -141,18 +143,18 @@ mod internal {
|
|||
if LAST_PALETTE.read().expect("Read last palette").as_ref()
|
||||
!= Some(&palette)
|
||||
{
|
||||
BEACON.log(client::Event::ThemeChanged(palette));
|
||||
log(client::Event::ThemeChanged(palette));
|
||||
|
||||
*LAST_PALETTE.write().expect("Write last palette") = Some(palette);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tasks_spawned(amount: usize) {
|
||||
BEACON.log(client::Event::CommandsSpawned(amount));
|
||||
log(client::Event::CommandsSpawned(amount));
|
||||
}
|
||||
|
||||
pub fn subscriptions_tracked(amount: usize) {
|
||||
BEACON.log(client::Event::SubscriptionsTracked(amount));
|
||||
log(client::Event::SubscriptionsTracked(amount));
|
||||
}
|
||||
|
||||
pub fn boot() -> Span {
|
||||
|
|
@ -162,6 +164,8 @@ mod internal {
|
|||
pub fn update(message: &impl std::fmt::Debug) -> Span {
|
||||
let span = span(span::Stage::Update);
|
||||
|
||||
let number = LAST_UPDATE.fetch_add(1, atomic::Ordering::Relaxed);
|
||||
|
||||
let start = Instant::now();
|
||||
let message = format!("{message:?}");
|
||||
let elapsed = start.elapsed();
|
||||
|
|
@ -172,11 +176,13 @@ mod internal {
|
|||
);
|
||||
}
|
||||
|
||||
BEACON.log(client::Event::MessageLogged(if message.len() > 49 {
|
||||
let message = if message.len() > 49 {
|
||||
format!("{}...", &message[..49])
|
||||
} else {
|
||||
message
|
||||
}));
|
||||
};
|
||||
|
||||
log(client::Event::MessageLogged { number, message });
|
||||
|
||||
span
|
||||
}
|
||||
|
|
@ -213,12 +219,35 @@ mod internal {
|
|||
span(span::Stage::Custom(name.into()))
|
||||
}
|
||||
|
||||
pub fn skip_next_timing() {
|
||||
SKIP_NEXT_SPAN.store(true, atomic::Ordering::Relaxed);
|
||||
pub fn enable() {
|
||||
ENABLED.store(true, atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn disable() {
|
||||
ENABLED.store(false, atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn commands() -> Subscription<Command> {
|
||||
fn listen_for_commands() -> impl Stream<Item = Command> {
|
||||
use crate::futures::futures::stream;
|
||||
|
||||
stream::unfold(BEACON.subscribe(), async move |mut receiver| {
|
||||
let command = match receiver.recv().await? {
|
||||
client::Command::RewindTo { message } => {
|
||||
Command::RewindTo { message }
|
||||
}
|
||||
client::Command::GoLive => Command::GoLive,
|
||||
};
|
||||
|
||||
Some((command, receiver))
|
||||
})
|
||||
}
|
||||
|
||||
Subscription::run(listen_for_commands)
|
||||
}
|
||||
|
||||
fn span(span: span::Stage) -> Span {
|
||||
BEACON.log(client::Event::SpanStarted(span.clone()));
|
||||
log(client::Event::SpanStarted(span.clone()));
|
||||
|
||||
Span {
|
||||
span,
|
||||
|
|
@ -236,6 +265,12 @@ mod internal {
|
|||
}
|
||||
}
|
||||
|
||||
fn log(event: client::Event) {
|
||||
if ENABLED.load(atomic::Ordering::Relaxed) {
|
||||
BEACON.log(event);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Span {
|
||||
span: span::Stage,
|
||||
|
|
@ -244,14 +279,7 @@ mod internal {
|
|||
|
||||
impl Span {
|
||||
pub fn finish(self) {
|
||||
if SKIP_NEXT_SPAN.fetch_and(false, atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
BEACON.log(client::Event::SpanFinished(
|
||||
self.span,
|
||||
self.start.elapsed(),
|
||||
));
|
||||
log(client::Event::SpanFinished(self.span, self.start.elapsed()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,22 +288,27 @@ mod internal {
|
|||
});
|
||||
|
||||
static NAME: RwLock<String> = RwLock::new(String::new());
|
||||
static LAST_UPDATE: AtomicUsize = AtomicUsize::new(0);
|
||||
static LAST_PALETTE: RwLock<Option<theme::Palette>> = RwLock::new(None);
|
||||
static SKIP_NEXT_SPAN: AtomicBool = AtomicBool::new(false);
|
||||
static ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "enable"), target_arch = "wasm32"))]
|
||||
mod internal {
|
||||
use crate::Primitive;
|
||||
use crate::core::theme;
|
||||
use crate::core::window;
|
||||
use crate::futures::Subscription;
|
||||
use crate::{Command, Primitive};
|
||||
|
||||
use std::io;
|
||||
|
||||
pub fn enable() {}
|
||||
pub fn disable() {}
|
||||
|
||||
pub fn init(_name: &str) {}
|
||||
|
||||
pub fn toggle_comet() -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
pub fn quit() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn theme_changed(_f: impl FnOnce() -> Option<theme::Palette>) {}
|
||||
|
|
@ -324,7 +357,9 @@ mod internal {
|
|||
Span
|
||||
}
|
||||
|
||||
pub fn skip_next_timing() {}
|
||||
pub fn commands() -> Subscription<Command> {
|
||||
Subscription::none()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Span;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,12 @@ rust-version.workspace = true
|
|||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
time-travel = ["iced_program/time-travel"]
|
||||
|
||||
[dependencies]
|
||||
iced_debug.workspace = true
|
||||
iced_program.workspace = true
|
||||
iced_widget.workspace = true
|
||||
iced_debug.workspace = true
|
||||
|
||||
log.workspace = true
|
||||
|
|
|
|||
140
devtools/src/comet.rs
Normal file
140
devtools/src/comet.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
use crate::executor;
|
||||
use crate::runtime::Task;
|
||||
|
||||
use std::process;
|
||||
|
||||
pub const COMPATIBLE_REVISION: &str =
|
||||
"69dd2283886dccdaa1ee6e1c274af62f7250bc38";
|
||||
|
||||
pub fn launch() -> Task<launch::Result> {
|
||||
executor::try_spawn_blocking(|mut sender| {
|
||||
let cargo_install = process::Command::new("cargo")
|
||||
.args(["install", "--list"])
|
||||
.output()?;
|
||||
|
||||
let installed_packages = String::from_utf8_lossy(&cargo_install.stdout);
|
||||
|
||||
for line in installed_packages.lines() {
|
||||
if !line.starts_with("iced_comet ") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some((_, revision)) = line.rsplit_once("?rev=") else {
|
||||
return Err(launch::Error::Outdated { revision: None });
|
||||
};
|
||||
|
||||
let Some((revision, _)) = revision.rsplit_once("#") else {
|
||||
return Err(launch::Error::Outdated { revision: None });
|
||||
};
|
||||
|
||||
if revision != COMPATIBLE_REVISION {
|
||||
return Err(launch::Error::Outdated {
|
||||
revision: Some(revision.to_owned()),
|
||||
});
|
||||
}
|
||||
|
||||
let _ = process::Command::new("iced_comet")
|
||||
.stdin(process::Stdio::null())
|
||||
.stdout(process::Stdio::null())
|
||||
.stderr(process::Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
let _ = sender.try_send(());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(launch::Error::NotFound)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn install() -> Task<install::Result> {
|
||||
executor::try_spawn_blocking(|mut sender| {
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
let mut install = Command::new("cargo")
|
||||
.args([
|
||||
"install",
|
||||
"--locked",
|
||||
"--git",
|
||||
"https://github.com/iced-rs/comet.git",
|
||||
"--rev",
|
||||
COMPATIBLE_REVISION,
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let mut stderr = BufReader::new(
|
||||
install.stderr.take().expect("stderr must be piped"),
|
||||
);
|
||||
|
||||
let mut log = String::new();
|
||||
|
||||
while let Ok(n) = stderr.read_line(&mut log) {
|
||||
if n == 0 {
|
||||
let status = install.wait()?;
|
||||
|
||||
if status.success() {
|
||||
break;
|
||||
} else {
|
||||
return Err(install::Error::ProcessFailed(status));
|
||||
}
|
||||
}
|
||||
|
||||
let _ = sender.try_send(install::Event::Logged(log.clone()));
|
||||
log.clear();
|
||||
}
|
||||
|
||||
let _ = sender.try_send(install::Event::Finished);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub mod launch {
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type Result = std::result::Result<(), Error>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Error {
|
||||
NotFound,
|
||||
Outdated { revision: Option<String> },
|
||||
IoFailed(Arc<io::Error>),
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(error: io::Error) -> Self {
|
||||
Self::IoFailed(Arc::new(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod install {
|
||||
use std::io;
|
||||
use std::process;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type Result = std::result::Result<Event, Error>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
Logged(String),
|
||||
Finished,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Error {
|
||||
ProcessFailed(process::ExitStatus),
|
||||
IoFailed(Arc<io::Error>),
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(error: io::Error) -> Self {
|
||||
Self::IoFailed(Arc::new(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
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;
|
||||
|
|
@ -17,3 +19,25 @@ where
|
|||
|
||||
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()),
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,23 +6,26 @@ use iced_widget::core;
|
|||
use iced_widget::runtime;
|
||||
use iced_widget::runtime::futures;
|
||||
|
||||
mod comet;
|
||||
mod executor;
|
||||
mod time_machine;
|
||||
|
||||
use crate::core::border;
|
||||
use crate::core::keyboard;
|
||||
use crate::core::theme::{self, Base, Theme};
|
||||
use crate::core::time::seconds;
|
||||
use crate::core::window;
|
||||
use crate::core::{Color, Element, Length::Fill};
|
||||
use crate::core::{Alignment::Center, Color, Element, Length::Fill};
|
||||
use crate::futures::Subscription;
|
||||
use crate::program::Program;
|
||||
use crate::runtime::Task;
|
||||
use crate::time_machine::TimeMachine;
|
||||
use crate::widget::{
|
||||
bottom_right, button, center, column, container, horizontal_space, opaque,
|
||||
row, scrollable, stack, text, themer,
|
||||
};
|
||||
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::thread;
|
||||
|
||||
pub fn attach(program: impl Program + 'static) -> impl Program {
|
||||
|
|
@ -115,15 +118,16 @@ where
|
|||
state: P::State,
|
||||
mode: Mode,
|
||||
show_notification: bool,
|
||||
time_machine: TimeMachine<P>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
HideNotification,
|
||||
ToggleComet,
|
||||
CometLaunched(comet::launch::Result),
|
||||
InstallComet,
|
||||
InstallationLogged(String),
|
||||
InstallationFinished,
|
||||
Installing(comet::install::Result),
|
||||
CancelSetup,
|
||||
}
|
||||
|
||||
|
|
@ -133,20 +137,26 @@ enum Mode {
|
|||
}
|
||||
|
||||
enum Setup {
|
||||
Idle,
|
||||
Idle { goal: Goal },
|
||||
Running { logs: Vec<String> },
|
||||
}
|
||||
|
||||
enum Goal {
|
||||
Installation,
|
||||
Update { revision: Option<String> },
|
||||
}
|
||||
|
||||
impl<P> DevTools<P>
|
||||
where
|
||||
P: Program + 'static,
|
||||
{
|
||||
pub fn new(state: P::State) -> (Self, Task<Message>) {
|
||||
fn new(state: P::State) -> (Self, Task<Message>) {
|
||||
(
|
||||
Self {
|
||||
state,
|
||||
mode: Mode::None,
|
||||
show_notification: true,
|
||||
time_machine: TimeMachine::new(),
|
||||
},
|
||||
executor::spawn_blocking(|mut sender| {
|
||||
thread::sleep(seconds(2));
|
||||
|
|
@ -156,11 +166,11 @@ where
|
|||
)
|
||||
}
|
||||
|
||||
pub fn title(&self, program: &P, window: window::Id) -> String {
|
||||
fn title(&self, program: &P, window: window::Id) -> String {
|
||||
program.title(&self.state, window)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, program: &P, event: Event<P>) -> Task<Event<P>> {
|
||||
fn update(&mut self, program: &P, event: Event<P>) -> Task<Event<P>> {
|
||||
match event {
|
||||
Event::Message(message) => match message {
|
||||
Message::HideNotification => {
|
||||
|
|
@ -170,12 +180,34 @@ where
|
|||
}
|
||||
Message::ToggleComet => {
|
||||
if let Mode::Setup(setup) = &self.mode {
|
||||
if matches!(setup, Setup::Idle) {
|
||||
if matches!(setup, Setup::Idle { .. }) {
|
||||
self.mode = Mode::None;
|
||||
}
|
||||
} else if let Err(error) = debug::toggle_comet() {
|
||||
if error.kind() == io::ErrorKind::NotFound {
|
||||
self.mode = Mode::Setup(Setup::Idle);
|
||||
|
||||
Task::none()
|
||||
} else if debug::quit() {
|
||||
Task::none()
|
||||
} else {
|
||||
comet::launch()
|
||||
.map(Message::CometLaunched)
|
||||
.map(Event::Message)
|
||||
}
|
||||
}
|
||||
Message::CometLaunched(Ok(())) => Task::none(),
|
||||
Message::CometLaunched(Err(error)) => {
|
||||
match error {
|
||||
comet::launch::Error::NotFound => {
|
||||
self.mode = Mode::Setup(Setup::Idle {
|
||||
goal: Goal::Installation,
|
||||
});
|
||||
}
|
||||
comet::launch::Error::Outdated { revision } => {
|
||||
self.mode = Mode::Setup(Setup::Idle {
|
||||
goal: Goal::Update { revision },
|
||||
});
|
||||
}
|
||||
comet::launch::Error::IoFailed(error) => {
|
||||
log::error!("comet failed to run: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -185,61 +217,42 @@ where
|
|||
self.mode =
|
||||
Mode::Setup(Setup::Running { logs: Vec::new() });
|
||||
|
||||
executor::spawn_blocking(|mut sender| {
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process::{Command, Stdio};
|
||||
comet::install()
|
||||
.map(Message::Installing)
|
||||
.map(Event::Message)
|
||||
}
|
||||
|
||||
let Ok(install) = Command::new("cargo")
|
||||
.args([
|
||||
"install",
|
||||
"--locked",
|
||||
"--git",
|
||||
"https://github.com/iced-rs/comet.git",
|
||||
"--rev",
|
||||
"eb114ba564a872acbd95e337d13e55f5f667b2f3",
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
Message::Installing(Ok(installation)) => {
|
||||
let Mode::Setup(Setup::Running { logs }) = &mut self.mode
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
let mut stderr = BufReader::new(
|
||||
install.stderr.expect("stderr must be piped"),
|
||||
);
|
||||
|
||||
let mut log = String::new();
|
||||
|
||||
while let Ok(n) = stderr.read_line(&mut log) {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let _ = sender.try_send(
|
||||
Message::InstallationLogged(log.clone()),
|
||||
);
|
||||
|
||||
log.clear();
|
||||
match installation {
|
||||
comet::install::Event::Logged(log) => {
|
||||
logs.push(log);
|
||||
Task::none()
|
||||
}
|
||||
comet::install::Event::Finished => {
|
||||
self.mode = Mode::None;
|
||||
comet::launch().discard()
|
||||
}
|
||||
|
||||
let _ = sender.try_send(Message::InstallationFinished);
|
||||
})
|
||||
.map(Event::Message)
|
||||
}
|
||||
Message::InstallationLogged(log) => {
|
||||
if let Mode::Setup(Setup::Running { logs }) = &mut self.mode
|
||||
{
|
||||
logs.push(log);
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::InstallationFinished => {
|
||||
self.mode = Mode::None;
|
||||
Message::Installing(Err(error)) => {
|
||||
let Mode::Setup(Setup::Running { logs }) = &mut self.mode
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
let _ = debug::toggle_comet();
|
||||
match error {
|
||||
comet::install::Error::ProcessFailed(status) => {
|
||||
logs.push(format!("process failed with {status}"));
|
||||
}
|
||||
comet::install::Error::IoFailed(error) => {
|
||||
logs.push(error.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
|
|
@ -250,23 +263,62 @@ where
|
|||
}
|
||||
},
|
||||
Event::Program(message) => {
|
||||
program.update(&mut self.state, message).map(Event::Program)
|
||||
self.time_machine.push(&message);
|
||||
|
||||
if self.time_machine.is_rewinding() {
|
||||
debug::enable();
|
||||
}
|
||||
|
||||
let span = debug::update(&message);
|
||||
let task = program.update(&mut self.state, message);
|
||||
debug::tasks_spawned(task.units());
|
||||
span.finish();
|
||||
|
||||
if self.time_machine.is_rewinding() {
|
||||
debug::disable();
|
||||
}
|
||||
|
||||
task.map(Event::Program)
|
||||
}
|
||||
Event::Command(command) => {
|
||||
match command {
|
||||
debug::Command::RewindTo { message } => {
|
||||
self.time_machine.rewind(program, message);
|
||||
}
|
||||
debug::Command::GoLive => {
|
||||
self.time_machine.go_to_present();
|
||||
}
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Event::Discard => Task::none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(
|
||||
fn view(
|
||||
&self,
|
||||
program: &P,
|
||||
window: window::Id,
|
||||
) -> Element<'_, Event<P>, P::Theme, P::Renderer> {
|
||||
let view = program.view(&self.state, window).map(Event::Program);
|
||||
let theme = program.theme(&self.state, window);
|
||||
let state = self.state();
|
||||
|
||||
let view = {
|
||||
let view = program.view(state, window);
|
||||
|
||||
if self.time_machine.is_rewinding() {
|
||||
view.map(|_| Event::Discard)
|
||||
} else {
|
||||
view.map(Event::Program)
|
||||
}
|
||||
};
|
||||
|
||||
let theme = program.theme(state, window);
|
||||
|
||||
let derive_theme = move || {
|
||||
theme
|
||||
.palette()
|
||||
.map(|palette| Theme::custom("DevTools".to_owned(), palette))
|
||||
.map(|palette| Theme::custom("iced devtools", palette))
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
|
|
@ -274,68 +326,14 @@ where
|
|||
Mode::None => None,
|
||||
Mode::Setup(setup) => {
|
||||
let stage: Element<'_, _, Theme, P::Renderer> = match setup {
|
||||
Setup::Idle => {
|
||||
let controls = row![
|
||||
button(text("Cancel").center().width(Fill))
|
||||
.width(100)
|
||||
.on_press(Message::CancelSetup)
|
||||
.style(button::danger),
|
||||
horizontal_space(),
|
||||
button(text("Install").center().width(Fill))
|
||||
.width(100)
|
||||
.on_press(Message::InstallComet)
|
||||
.style(button::success),
|
||||
];
|
||||
|
||||
column![
|
||||
text("comet is not installed!").size(20),
|
||||
"In order to display performance metrics, the \
|
||||
comet debugger must be installed in your system.",
|
||||
"The comet debugger is an official companion tool \
|
||||
that helps you debug your iced applications.",
|
||||
"Do you wish to install it with the following \
|
||||
command?",
|
||||
container(
|
||||
text(
|
||||
"cargo install --locked \
|
||||
--git https://github.com/iced-rs/comet.git"
|
||||
)
|
||||
.size(14)
|
||||
)
|
||||
.width(Fill)
|
||||
.padding(5)
|
||||
.style(container::dark),
|
||||
controls,
|
||||
]
|
||||
.spacing(20)
|
||||
.into()
|
||||
}
|
||||
Setup::Running { logs } => column![
|
||||
text("Installing comet...").size(20),
|
||||
container(
|
||||
scrollable(
|
||||
column(
|
||||
logs.iter()
|
||||
.map(|log| text(log).size(12).into()),
|
||||
)
|
||||
.spacing(3),
|
||||
)
|
||||
.spacing(10)
|
||||
.width(Fill)
|
||||
.height(300)
|
||||
.anchor_bottom(),
|
||||
)
|
||||
.padding(10)
|
||||
.style(container::dark)
|
||||
]
|
||||
.spacing(20)
|
||||
.into(),
|
||||
Setup::Idle { goal } => self::setup(goal),
|
||||
Setup::Running { logs } => installation(logs),
|
||||
};
|
||||
|
||||
let setup = center(
|
||||
container(stage)
|
||||
.padding(20)
|
||||
.width(500)
|
||||
.max_width(500)
|
||||
.style(container::bordered_box),
|
||||
)
|
||||
.padding(10)
|
||||
|
|
@ -363,14 +361,16 @@ where
|
|||
});
|
||||
|
||||
stack![view]
|
||||
.height(Fill)
|
||||
.push_maybe(mode.map(opaque))
|
||||
.push_maybe(notification)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn subscription(&self, program: &P) -> Subscription<Event<P>> {
|
||||
fn subscription(&self, program: &P) -> Subscription<Event<P>> {
|
||||
let subscription =
|
||||
program.subscription(&self.state).map(Event::Program);
|
||||
debug::subscriptions_tracked(subscription.units());
|
||||
|
||||
let hotkeys =
|
||||
futures::keyboard::on_key_press(|key, _modifiers| match key {
|
||||
|
|
@ -381,19 +381,25 @@ where
|
|||
})
|
||||
.map(Event::Message);
|
||||
|
||||
Subscription::batch([subscription, hotkeys])
|
||||
let commands = debug::commands().map(Event::Command);
|
||||
|
||||
Subscription::batch([subscription, hotkeys, commands])
|
||||
}
|
||||
|
||||
pub fn theme(&self, program: &P, window: window::Id) -> P::Theme {
|
||||
program.theme(&self.state, window)
|
||||
fn theme(&self, program: &P, window: window::Id) -> P::Theme {
|
||||
program.theme(self.state(), window)
|
||||
}
|
||||
|
||||
pub fn style(&self, program: &P, theme: &P::Theme) -> theme::Style {
|
||||
program.style(&self.state, theme)
|
||||
fn style(&self, program: &P, theme: &P::Theme) -> theme::Style {
|
||||
program.style(self.state(), theme)
|
||||
}
|
||||
|
||||
pub fn scale_factor(&self, program: &P, window: window::Id) -> f64 {
|
||||
program.scale_factor(&self.state, window)
|
||||
fn scale_factor(&self, program: &P, window: window::Id) -> f64 {
|
||||
program.scale_factor(self.state(), window)
|
||||
}
|
||||
|
||||
fn state(&self) -> &P::State {
|
||||
self.time_machine.state().unwrap_or(&self.state)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -403,6 +409,8 @@ where
|
|||
{
|
||||
Message(Message),
|
||||
Program(P::Message),
|
||||
Command(debug::Command),
|
||||
Discard,
|
||||
}
|
||||
|
||||
impl<P> fmt::Debug for Event<P>
|
||||
|
|
@ -413,6 +421,154 @@ where
|
|||
match self {
|
||||
Self::Message(message) => message.fmt(f),
|
||||
Self::Program(message) => message.fmt(f),
|
||||
Self::Command(command) => command.fmt(f),
|
||||
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::Discard => Self::Discard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup<Renderer>(goal: &Goal) -> Element<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::text::Renderer + 'static,
|
||||
{
|
||||
let controls = row![
|
||||
button(text("Cancel").center().width(Fill))
|
||||
.width(100)
|
||||
.on_press(Message::CancelSetup)
|
||||
.style(button::danger),
|
||||
horizontal_space(),
|
||||
button(
|
||||
text(match goal {
|
||||
Goal::Installation => "Install",
|
||||
Goal::Update { .. } => "Update",
|
||||
})
|
||||
.center()
|
||||
.width(Fill)
|
||||
)
|
||||
.width(100)
|
||||
.on_press(Message::InstallComet)
|
||||
.style(button::success),
|
||||
];
|
||||
|
||||
let command = container(
|
||||
text!(
|
||||
"cargo install --locked \\
|
||||
--git https://github.com/iced-rs/comet.git \\
|
||||
--rev {}",
|
||||
comet::COMPATIBLE_REVISION
|
||||
)
|
||||
.size(14)
|
||||
.font(Renderer::MONOSPACE_FONT),
|
||||
)
|
||||
.width(Fill)
|
||||
.padding(5)
|
||||
.style(container::dark);
|
||||
|
||||
Element::from(match goal {
|
||||
Goal::Installation => column![
|
||||
text("comet is not installed!").size(20),
|
||||
"In order to display performance \
|
||||
metrics, the comet debugger must \
|
||||
be installed in your system.",
|
||||
"The comet debugger is an official \
|
||||
companion tool that helps you debug \
|
||||
your iced applications.",
|
||||
column![
|
||||
"Do you wish to install it with the \
|
||||
following command?",
|
||||
command
|
||||
]
|
||||
.spacing(10),
|
||||
controls,
|
||||
]
|
||||
.spacing(20),
|
||||
Goal::Update { revision } => {
|
||||
let comparison = column![
|
||||
row![
|
||||
"Installed revision:",
|
||||
horizontal_space(),
|
||||
inline_code(revision.as_deref().unwrap_or("Unknown"))
|
||||
]
|
||||
.align_y(Center),
|
||||
row![
|
||||
"Compatible revision:",
|
||||
horizontal_space(),
|
||||
inline_code(comet::COMPATIBLE_REVISION),
|
||||
]
|
||||
.align_y(Center)
|
||||
]
|
||||
.spacing(5);
|
||||
|
||||
column![
|
||||
text("comet is out of date!").size(20),
|
||||
comparison,
|
||||
column![
|
||||
"Do you wish to update it with the following \
|
||||
command?",
|
||||
command
|
||||
]
|
||||
.spacing(10),
|
||||
controls,
|
||||
]
|
||||
.spacing(20)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn installation<'a, Renderer>(
|
||||
logs: &'a [String],
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::text::Renderer + 'a,
|
||||
{
|
||||
column![
|
||||
text("Installing comet...").size(20),
|
||||
container(
|
||||
scrollable(
|
||||
column(logs.iter().map(|log| {
|
||||
text(log).size(12).font(Renderer::MONOSPACE_FONT).into()
|
||||
}),)
|
||||
.spacing(3),
|
||||
)
|
||||
.spacing(10)
|
||||
.width(Fill)
|
||||
.height(300)
|
||||
.anchor_bottom(),
|
||||
)
|
||||
.padding(10)
|
||||
.style(container::dark)
|
||||
]
|
||||
.spacing(20)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn inline_code<'a, Renderer>(
|
||||
code: impl text::IntoFragment<'a>,
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::text::Renderer + 'a,
|
||||
{
|
||||
container(text(code).font(Renderer::MONOSPACE_FONT).size(12))
|
||||
.style(|_theme| {
|
||||
container::Style::default()
|
||||
.background(Color::BLACK)
|
||||
.border(border::rounded(2))
|
||||
})
|
||||
.padding([2, 4])
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
88
devtools/src/time_machine.rs
Normal file
88
devtools/src/time_machine.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
use crate::Program;
|
||||
|
||||
#[cfg(feature = "time-travel")]
|
||||
pub struct TimeMachine<P>
|
||||
where
|
||||
P: Program,
|
||||
{
|
||||
state: Option<P::State>,
|
||||
messages: Vec<P::Message>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "time-travel")]
|
||||
impl<P> TimeMachine<P>
|
||||
where
|
||||
P: Program,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: None,
|
||||
messages: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_rewinding(&self) -> bool {
|
||||
self.state.is_some()
|
||||
}
|
||||
|
||||
pub fn push(&mut self, message: &P::Message) {
|
||||
self.messages.push(message.clone());
|
||||
}
|
||||
|
||||
pub fn rewind(&mut self, program: &P, message: usize) {
|
||||
let (mut state, _) = program.boot();
|
||||
|
||||
if message < self.messages.len() {
|
||||
// TODO: Run concurrently (?)
|
||||
for message in &self.messages[0..message] {
|
||||
let _ = program.update(&mut state, message.clone());
|
||||
}
|
||||
}
|
||||
|
||||
self.state = Some(state);
|
||||
crate::debug::disable();
|
||||
}
|
||||
|
||||
pub fn go_to_present(&mut self) {
|
||||
self.state = None;
|
||||
crate::debug::enable();
|
||||
}
|
||||
|
||||
pub fn state(&self) -> Option<&P::State> {
|
||||
self.state.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "time-travel"))]
|
||||
pub struct TimeMachine<P>
|
||||
where
|
||||
P: Program,
|
||||
{
|
||||
_program: std::marker::PhantomData<P>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "time-travel"))]
|
||||
impl<P> TimeMachine<P>
|
||||
where
|
||||
P: Program,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
_program: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_rewinding(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn push(&mut self, _message: &P::Message) {}
|
||||
|
||||
pub fn rewind(&mut self, _program: &P, _message: usize) {}
|
||||
|
||||
pub fn go_to_present(&mut self) {}
|
||||
|
||||
pub fn state(&self) -> Option<&P::State> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ use iced::window;
|
|||
use iced::{Element, Fill, Point, Rectangle, Renderer, Subscription, Theme};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
iced::application(Arc::default, Arc::update, Arc::view)
|
||||
iced::application(Arc::new, Arc::update, Arc::view)
|
||||
.subscription(Arc::subscription)
|
||||
.theme(|_| Theme::Dark)
|
||||
.run()
|
||||
|
|
@ -25,6 +25,13 @@ enum Message {
|
|||
}
|
||||
|
||||
impl Arc {
|
||||
fn new() -> Self {
|
||||
Arc {
|
||||
start: Instant::now(),
|
||||
cache: Cache::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _: Message) {
|
||||
self.cache.clear();
|
||||
}
|
||||
|
|
@ -38,15 +45,6 @@ impl Arc {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Arc {
|
||||
fn default() -> Self {
|
||||
Arc {
|
||||
start: Instant::now(),
|
||||
cache: Cache::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message> canvas::Program<Message> for Arc {
|
||||
type State = ();
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use iced::{
|
|||
pub fn main() -> iced::Result {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
iced::application(Clock::default, Clock::update, Clock::view)
|
||||
iced::application(Clock::new, Clock::update, Clock::view)
|
||||
.subscription(Clock::subscription)
|
||||
.theme(Clock::theme)
|
||||
.run()
|
||||
|
|
@ -28,6 +28,13 @@ enum Message {
|
|||
}
|
||||
|
||||
impl Clock {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
now: chrono::offset::Local::now(),
|
||||
clock: Cache::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::Tick(local_time) => {
|
||||
|
|
@ -58,15 +65,6 @@ impl Clock {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Clock {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
now: chrono::offset::Local::now(),
|
||||
clock: Cache::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message> canvas::Program<Message> for Clock {
|
||||
type State = ();
|
||||
|
||||
|
|
|
|||
|
|
@ -21,14 +21,15 @@ use iced::{
|
|||
use std::collections::HashMap;
|
||||
|
||||
fn main() -> iced::Result {
|
||||
iced::application(Gallery::new, Gallery::update, Gallery::view)
|
||||
.window_size((
|
||||
Preview::WIDTH as f32 * 4.0,
|
||||
Preview::HEIGHT as f32 * 2.5,
|
||||
))
|
||||
.subscription(Gallery::subscription)
|
||||
.theme(Gallery::theme)
|
||||
.run()
|
||||
iced::application::timed(
|
||||
Gallery::new,
|
||||
Gallery::update,
|
||||
Gallery::subscription,
|
||||
Gallery::view,
|
||||
)
|
||||
.window_size((Preview::WIDTH as f32 * 4.0, Preview::HEIGHT as f32 * 2.5))
|
||||
.theme(Gallery::theme)
|
||||
.run()
|
||||
}
|
||||
|
||||
struct Gallery {
|
||||
|
|
@ -48,7 +49,7 @@ enum Message {
|
|||
BlurhashDecoded(Id, civitai::Blurhash),
|
||||
Open(Id),
|
||||
Close,
|
||||
Animate(Instant),
|
||||
Animate,
|
||||
}
|
||||
|
||||
impl Gallery {
|
||||
|
|
@ -76,13 +77,15 @@ impl Gallery {
|
|||
|| self.viewer.is_animating(self.now);
|
||||
|
||||
if is_animating {
|
||||
window::frames().map(Message::Animate)
|
||||
window::frames().map(|_| Message::Animate)
|
||||
} else {
|
||||
Subscription::none()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) -> Task<Message> {
|
||||
pub fn update(&mut self, message: Message, now: Instant) -> Task<Message> {
|
||||
self.now = now;
|
||||
|
||||
match message {
|
||||
Message::ImagesListed(Ok(images)) => {
|
||||
self.images = images;
|
||||
|
|
@ -109,16 +112,16 @@ impl Gallery {
|
|||
)
|
||||
}
|
||||
Message::ImageDownloaded(Ok(rgba)) => {
|
||||
self.viewer.show(rgba);
|
||||
self.viewer.show(rgba, self.now);
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::ThumbnailDownloaded(id, Ok(rgba)) => {
|
||||
let thumbnail = if let Some(preview) = self.previews.remove(&id)
|
||||
{
|
||||
preview.load(rgba)
|
||||
preview.load(rgba, self.now)
|
||||
} else {
|
||||
Preview::ready(rgba)
|
||||
Preview::ready(rgba, self.now)
|
||||
};
|
||||
|
||||
let _ = self.previews.insert(id, thumbnail);
|
||||
|
|
@ -127,7 +130,7 @@ impl Gallery {
|
|||
}
|
||||
Message::ThumbnailHovered(id, is_hovered) => {
|
||||
if let Some(preview) = self.previews.get_mut(&id) {
|
||||
preview.toggle_zoom(is_hovered);
|
||||
preview.toggle_zoom(is_hovered, self.now);
|
||||
}
|
||||
|
||||
Task::none()
|
||||
|
|
@ -136,7 +139,7 @@ impl Gallery {
|
|||
if !self.previews.contains_key(&id) {
|
||||
let _ = self
|
||||
.previews
|
||||
.insert(id, Preview::loading(blurhash.rgba));
|
||||
.insert(id, Preview::loading(blurhash.rgba, self.now));
|
||||
}
|
||||
|
||||
Task::none()
|
||||
|
|
@ -151,7 +154,7 @@ impl Gallery {
|
|||
return Task::none();
|
||||
};
|
||||
|
||||
self.viewer.open();
|
||||
self.viewer.open(self.now);
|
||||
|
||||
Task::perform(
|
||||
image.download(Size::Original),
|
||||
|
|
@ -159,15 +162,11 @@ impl Gallery {
|
|||
)
|
||||
}
|
||||
Message::Close => {
|
||||
self.viewer.close();
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::Animate(now) => {
|
||||
self.now = now;
|
||||
self.viewer.close(self.now);
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::Animate => Task::none(),
|
||||
Message::ImagesListed(Err(error))
|
||||
| Message::ImageDownloaded(Err(error))
|
||||
| Message::ThumbnailDownloaded(_, Err(error)) => {
|
||||
|
|
@ -293,13 +292,13 @@ impl Preview {
|
|||
const WIDTH: u32 = 320;
|
||||
const HEIGHT: u32 = 410;
|
||||
|
||||
fn loading(rgba: Rgba) -> Self {
|
||||
fn loading(rgba: Rgba, now: Instant) -> Self {
|
||||
Self::Loading {
|
||||
blurhash: Blurhash {
|
||||
fade_in: Animation::new(false)
|
||||
.duration(milliseconds(700))
|
||||
.easing(animation::Easing::EaseIn)
|
||||
.go(true),
|
||||
.go(true, now),
|
||||
handle: image::Handle::from_rgba(
|
||||
rgba.width,
|
||||
rgba.height,
|
||||
|
|
@ -309,27 +308,27 @@ impl Preview {
|
|||
}
|
||||
}
|
||||
|
||||
fn ready(rgba: Rgba) -> Self {
|
||||
fn ready(rgba: Rgba, now: Instant) -> Self {
|
||||
Self::Ready {
|
||||
blurhash: None,
|
||||
thumbnail: Thumbnail::new(rgba),
|
||||
thumbnail: Thumbnail::new(rgba, now),
|
||||
}
|
||||
}
|
||||
|
||||
fn load(self, rgba: Rgba) -> Self {
|
||||
fn load(self, rgba: Rgba, now: Instant) -> Self {
|
||||
let Self::Loading { blurhash } = self else {
|
||||
return self;
|
||||
};
|
||||
|
||||
Self::Ready {
|
||||
blurhash: Some(blurhash),
|
||||
thumbnail: Thumbnail::new(rgba),
|
||||
thumbnail: Thumbnail::new(rgba, now),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_zoom(&mut self, enabled: bool) {
|
||||
fn toggle_zoom(&mut self, enabled: bool, now: Instant) {
|
||||
if let Self::Ready { thumbnail, .. } = self {
|
||||
thumbnail.zoom.go_mut(enabled);
|
||||
thumbnail.zoom.go_mut(enabled, now);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -357,14 +356,14 @@ impl Preview {
|
|||
}
|
||||
|
||||
impl Thumbnail {
|
||||
pub fn new(rgba: Rgba) -> Self {
|
||||
pub fn new(rgba: Rgba, now: Instant) -> Self {
|
||||
Self {
|
||||
handle: image::Handle::from_rgba(
|
||||
rgba.width,
|
||||
rgba.height,
|
||||
rgba.pixels,
|
||||
),
|
||||
fade_in: Animation::new(false).slow().go(true),
|
||||
fade_in: Animation::new(false).slow().go(true, now),
|
||||
zoom: Animation::new(false)
|
||||
.quick()
|
||||
.easing(animation::Easing::EaseInOut),
|
||||
|
|
@ -391,24 +390,24 @@ impl Viewer {
|
|||
}
|
||||
}
|
||||
|
||||
fn open(&mut self) {
|
||||
fn open(&mut self, now: Instant) {
|
||||
self.image = None;
|
||||
self.background_fade_in.go_mut(true);
|
||||
self.background_fade_in.go_mut(true, now);
|
||||
}
|
||||
|
||||
fn show(&mut self, rgba: Rgba) {
|
||||
fn show(&mut self, rgba: Rgba, now: Instant) {
|
||||
self.image = Some(image::Handle::from_rgba(
|
||||
rgba.width,
|
||||
rgba.height,
|
||||
rgba.pixels,
|
||||
));
|
||||
self.background_fade_in.go_mut(true);
|
||||
self.image_fade_in.go_mut(true);
|
||||
self.background_fade_in.go_mut(true, now);
|
||||
self.image_fade_in.go_mut(true, now);
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.background_fade_in.go_mut(false);
|
||||
self.image_fade_in.go_mut(false);
|
||||
fn close(&mut self, now: Instant) {
|
||||
self.background_fade_in.go_mut(false, now);
|
||||
self.image_fade_in.go_mut(false, now);
|
||||
}
|
||||
|
||||
fn is_animating(&self, now: Instant) -> bool {
|
||||
|
|
|
|||
|
|
@ -18,11 +18,15 @@ use std::io;
|
|||
use std::sync::Arc;
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
iced::application(Markdown::new, Markdown::update, Markdown::view)
|
||||
.font(icon::FONT)
|
||||
.subscription(Markdown::subscription)
|
||||
.theme(Markdown::theme)
|
||||
.run()
|
||||
iced::application::timed(
|
||||
Markdown::new,
|
||||
Markdown::update,
|
||||
Markdown::subscription,
|
||||
Markdown::view,
|
||||
)
|
||||
.font(icon::FONT)
|
||||
.theme(Markdown::theme)
|
||||
.run()
|
||||
}
|
||||
|
||||
struct Markdown {
|
||||
|
|
@ -58,7 +62,7 @@ enum Message {
|
|||
ImageDownloaded(markdown::Url, Result<image::Handle, Error>),
|
||||
ToggleStream(bool),
|
||||
NextToken,
|
||||
Animate(Instant),
|
||||
Tick,
|
||||
}
|
||||
|
||||
impl Markdown {
|
||||
|
|
@ -78,7 +82,9 @@ impl Markdown {
|
|||
)
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
fn update(&mut self, message: Message, now: Instant) -> Task<Message> {
|
||||
self.now = now;
|
||||
|
||||
match message {
|
||||
Message::Edit(action) => {
|
||||
let is_edit = action.is_edit();
|
||||
|
|
@ -119,7 +125,7 @@ impl Markdown {
|
|||
fade_in: Animation::new(false)
|
||||
.quick()
|
||||
.easing(animation::Easing::EaseInOut)
|
||||
.go(true),
|
||||
.go(true, self.now),
|
||||
})
|
||||
.unwrap_or_else(Image::Errored),
|
||||
);
|
||||
|
|
@ -164,11 +170,7 @@ impl Markdown {
|
|||
|
||||
Task::none()
|
||||
}
|
||||
Message::Animate(now) => {
|
||||
self.now = now;
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::Tick => Task::none(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,7 +232,7 @@ impl Markdown {
|
|||
});
|
||||
|
||||
if is_animating {
|
||||
window::frames().map(Message::Animate)
|
||||
window::frames().map(|_| Message::Tick)
|
||||
} else {
|
||||
Subscription::none()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ struct Multitouch {
|
|||
fingers: HashMap<touch::Finger, Point>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
FingerPressed { id: touch::Finger, position: Point },
|
||||
FingerLifted { id: touch::Finger },
|
||||
|
|
|
|||
|
|
@ -21,31 +21,36 @@ use std::time::Instant;
|
|||
pub fn main() -> iced::Result {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
iced::application(
|
||||
SolarSystem::default,
|
||||
iced::application::timed(
|
||||
SolarSystem::new,
|
||||
SolarSystem::update,
|
||||
SolarSystem::subscription,
|
||||
SolarSystem::view,
|
||||
)
|
||||
.subscription(SolarSystem::subscription)
|
||||
.theme(SolarSystem::theme)
|
||||
.run()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SolarSystem {
|
||||
state: State,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Message {
|
||||
Tick(Instant),
|
||||
Tick,
|
||||
}
|
||||
|
||||
impl SolarSystem {
|
||||
fn update(&mut self, message: Message) {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
state: State::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message, now: Instant) {
|
||||
match message {
|
||||
Message::Tick(instant) => {
|
||||
self.state.update(instant);
|
||||
Message::Tick => {
|
||||
self.state.update(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +64,7 @@ impl SolarSystem {
|
|||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
window::frames().map(Message::Tick)
|
||||
window::frames().map(|_| Message::Tick)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,6 +110,7 @@ impl State {
|
|||
}
|
||||
|
||||
pub fn update(&mut self, now: Instant) {
|
||||
self.start = self.start.min(now);
|
||||
self.now = now;
|
||||
self.system_cache.clear();
|
||||
}
|
||||
|
|
@ -202,9 +208,3 @@ impl<Message> canvas::Program<Message> for State {
|
|||
vec![background, system]
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use iced::mouse;
|
||||
use iced::time::{self, Instant, milliseconds};
|
||||
use iced::time::{self, milliseconds};
|
||||
use iced::widget::canvas;
|
||||
use iced::{
|
||||
Color, Element, Fill, Font, Point, Rectangle, Renderer, Subscription, Theme,
|
||||
|
|
@ -22,13 +22,13 @@ struct TheMatrix {
|
|||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Message {
|
||||
Tick(Instant),
|
||||
Tick,
|
||||
}
|
||||
|
||||
impl TheMatrix {
|
||||
fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::Tick(_now) => {
|
||||
Message::Tick => {
|
||||
self.tick += 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ impl TheMatrix {
|
|||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
time::every(milliseconds(50)).map(Message::Tick)
|
||||
time::every(milliseconds(50)).map(|_| Message::Tick)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ publish = false
|
|||
|
||||
[dependencies]
|
||||
iced.workspace = true
|
||||
iced.features = ["tokio", "debug"]
|
||||
iced.features = ["tokio", "debug", "time-travel"]
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
|
|
|||
|
|
@ -285,6 +285,11 @@ impl<T> Subscription<T> {
|
|||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the amount of recipe units in this [`Subscription`].
|
||||
pub fn units(&self) -> usize {
|
||||
self.recipes.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`Subscription`] from a [`Recipe`] describing it.
|
||||
|
|
|
|||
|
|
@ -10,9 +10,12 @@ categories.workspace = true
|
|||
keywords.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
time-travel = []
|
||||
|
||||
[dependencies]
|
||||
iced_graphics.workspace = true
|
||||
iced_runtime.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ pub trait Program: Sized {
|
|||
type State;
|
||||
|
||||
/// The message of the program.
|
||||
type Message: Send + std::fmt::Debug + 'static;
|
||||
type Message: Message + 'static;
|
||||
|
||||
/// The theme of the program.
|
||||
type Theme: Default + theme::Base;
|
||||
|
|
@ -642,3 +642,17 @@ impl<P: Program> Instance<P> {
|
|||
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 {}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ where
|
|||
type Paragraph = A::Paragraph;
|
||||
type Editor = A::Editor;
|
||||
|
||||
const MONOSPACE_FONT: Self::Font = A::MONOSPACE_FONT;
|
||||
const ICON_FONT: Self::Font = A::ICON_FONT;
|
||||
const CHECKMARK_ICON: char = A::CHECKMARK_ICON;
|
||||
const ARROW_DOWN_ICON: char = A::ARROW_DOWN_ICON;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ use crate::{
|
|||
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub mod timed;
|
||||
|
||||
pub use timed::timed;
|
||||
|
||||
/// Creates an iced [`Application`] given its boot, update, and view logic.
|
||||
///
|
||||
/// # Example
|
||||
|
|
@ -75,7 +79,7 @@ pub fn application<State, Message, Theme, Renderer>(
|
|||
) -> Application<impl Program<State = State, Message = Message, Theme = Theme>>
|
||||
where
|
||||
State: 'static,
|
||||
Message: Send + std::fmt::Debug + 'static,
|
||||
Message: program::Message + 'static,
|
||||
Theme: Default + theme::Base,
|
||||
Renderer: program::Renderer,
|
||||
{
|
||||
|
|
@ -94,7 +98,7 @@ where
|
|||
impl<State, Message, Theme, Renderer, Boot, Update, View> Program
|
||||
for Instance<State, Message, Theme, Renderer, Boot, Update, View>
|
||||
where
|
||||
Message: Send + std::fmt::Debug + 'static,
|
||||
Message: program::Message + 'static,
|
||||
Theme: Default + theme::Base,
|
||||
Renderer: program::Renderer,
|
||||
Boot: self::Boot<State, Message>,
|
||||
|
|
|
|||
178
src/application/timed.rs
Normal file
178
src/application/timed.rs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
//! An [`Application`] that receives an [`Instant`] in update logic.
|
||||
use crate::application::{Application, Boot, View};
|
||||
use crate::program;
|
||||
use crate::theme;
|
||||
use crate::time::Instant;
|
||||
use crate::window;
|
||||
use crate::{Element, Program, Settings, Subscription, Task};
|
||||
|
||||
/// Creates an [`Application`] with an `update` function that also
|
||||
/// takes the [`Instant`] of each `Message`.
|
||||
///
|
||||
/// This constructor is useful to create animated applications that
|
||||
/// are _pure_ (e.g. without relying on side-effect calls like [`Instant::now`]).
|
||||
///
|
||||
/// Purity is needed when you want your application to end up in the
|
||||
/// same exact state given the same history of messages. This property
|
||||
/// enables proper time traveling debugging with [`comet`].
|
||||
///
|
||||
/// [`comet`]: https://github.com/iced-rs/comet
|
||||
pub fn timed<State, Message, Theme, Renderer>(
|
||||
boot: impl Boot<State, Message>,
|
||||
update: impl Update<State, Message>,
|
||||
subscription: impl Fn(&State) -> Subscription<Message>,
|
||||
view: impl for<'a> View<'a, State, Message, Theme, Renderer>,
|
||||
) -> Application<
|
||||
impl Program<State = State, Message = (Message, Instant), Theme = Theme>,
|
||||
>
|
||||
where
|
||||
State: 'static,
|
||||
Message: program::Message + 'static,
|
||||
Theme: Default + theme::Base + 'static,
|
||||
Renderer: program::Renderer + 'static,
|
||||
{
|
||||
use std::marker::PhantomData;
|
||||
|
||||
struct Instance<
|
||||
State,
|
||||
Message,
|
||||
Theme,
|
||||
Renderer,
|
||||
Boot,
|
||||
Update,
|
||||
Subscription,
|
||||
View,
|
||||
> {
|
||||
boot: Boot,
|
||||
update: Update,
|
||||
subscription: Subscription,
|
||||
view: View,
|
||||
_state: PhantomData<State>,
|
||||
_message: PhantomData<Message>,
|
||||
_theme: PhantomData<Theme>,
|
||||
_renderer: PhantomData<Renderer>,
|
||||
}
|
||||
|
||||
impl<State, Message, Theme, Renderer, Boot, Update, Subscription, View>
|
||||
Program
|
||||
for Instance<
|
||||
State,
|
||||
Message,
|
||||
Theme,
|
||||
Renderer,
|
||||
Boot,
|
||||
Update,
|
||||
Subscription,
|
||||
View,
|
||||
>
|
||||
where
|
||||
Message: program::Message + 'static,
|
||||
Theme: Default + theme::Base + 'static,
|
||||
Renderer: program::Renderer + 'static,
|
||||
Boot: self::Boot<State, Message>,
|
||||
Update: self::Update<State, Message>,
|
||||
Subscription: Fn(&State) -> self::Subscription<Message>,
|
||||
View: for<'a> self::View<'a, State, Message, Theme, Renderer>,
|
||||
{
|
||||
type State = State;
|
||||
type Message = (Message, Instant);
|
||||
type Theme = Theme;
|
||||
type Renderer = Renderer;
|
||||
type Executor = iced_futures::backend::default::Executor;
|
||||
|
||||
fn name() -> &'static str {
|
||||
let name = std::any::type_name::<State>();
|
||||
|
||||
name.split("::").next().unwrap_or("a_cool_application")
|
||||
}
|
||||
|
||||
fn boot(&self) -> (State, Task<Self::Message>) {
|
||||
let (state, task) = self.boot.boot();
|
||||
|
||||
(state, task.map(|message| (message, Instant::now())))
|
||||
}
|
||||
|
||||
fn update(
|
||||
&self,
|
||||
state: &mut Self::State,
|
||||
(message, now): Self::Message,
|
||||
) -> Task<Self::Message> {
|
||||
self.update
|
||||
.update(state, message, now)
|
||||
.into()
|
||||
.map(|message| (message, Instant::now()))
|
||||
}
|
||||
|
||||
fn view<'a>(
|
||||
&self,
|
||||
state: &'a Self::State,
|
||||
_window: window::Id,
|
||||
) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
|
||||
self.view
|
||||
.view(state)
|
||||
.into()
|
||||
.map(|message| (message, Instant::now()))
|
||||
}
|
||||
|
||||
fn subscription(
|
||||
&self,
|
||||
state: &Self::State,
|
||||
) -> self::Subscription<Self::Message> {
|
||||
(self.subscription)(state).map(|message| (message, Instant::now()))
|
||||
}
|
||||
}
|
||||
|
||||
Application {
|
||||
raw: Instance {
|
||||
boot,
|
||||
update,
|
||||
subscription,
|
||||
view,
|
||||
_state: PhantomData,
|
||||
_message: PhantomData,
|
||||
_theme: PhantomData,
|
||||
_renderer: PhantomData,
|
||||
},
|
||||
settings: Settings::default(),
|
||||
window: window::Settings::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The update logic of some timed [`Application`].
|
||||
///
|
||||
/// This is like [`application::Update`](super::Update),
|
||||
/// but it also takes an [`Instant`].
|
||||
pub trait Update<State, Message> {
|
||||
/// Processes the message and updates the state of the [`Application`].
|
||||
fn update(
|
||||
&self,
|
||||
state: &mut State,
|
||||
message: Message,
|
||||
now: Instant,
|
||||
) -> impl Into<Task<Message>>;
|
||||
}
|
||||
|
||||
impl<State, Message> Update<State, Message> for () {
|
||||
fn update(
|
||||
&self,
|
||||
_state: &mut State,
|
||||
_message: Message,
|
||||
_now: Instant,
|
||||
) -> impl Into<Task<Message>> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, State, Message, C> Update<State, Message> for T
|
||||
where
|
||||
T: Fn(&mut State, Message, Instant) -> C,
|
||||
C: Into<Task<Message>>,
|
||||
{
|
||||
fn update(
|
||||
&self,
|
||||
state: &mut State,
|
||||
message: Message,
|
||||
now: Instant,
|
||||
) -> impl Into<Task<Message>> {
|
||||
self(state, message, now)
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ pub fn daemon<State, Message, Theme, Renderer>(
|
|||
) -> Daemon<impl Program<State = State, Message = Message, Theme = Theme>>
|
||||
where
|
||||
State: 'static,
|
||||
Message: Send + std::fmt::Debug + 'static,
|
||||
Message: program::Message + 'static,
|
||||
Theme: Default + theme::Base,
|
||||
Renderer: program::Renderer,
|
||||
{
|
||||
|
|
@ -44,7 +44,7 @@ where
|
|||
impl<State, Message, Theme, Renderer, Boot, Update, View> Program
|
||||
for Instance<State, Message, Theme, Renderer, Boot, Update, View>
|
||||
where
|
||||
Message: Send + std::fmt::Debug + 'static,
|
||||
Message: program::Message + 'static,
|
||||
Theme: Default + theme::Base,
|
||||
Renderer: program::Renderer,
|
||||
Boot: application::Boot<State, Message>,
|
||||
|
|
|
|||
10
src/lib.rs
10
src/lib.rs
|
|
@ -343,7 +343,7 @@
|
|||
//! use iced::window;
|
||||
//! use iced::{Size, Subscription};
|
||||
//!
|
||||
//! #[derive(Debug)]
|
||||
//! #[derive(Debug, Clone)]
|
||||
//! enum Message {
|
||||
//! WindowResized(Size),
|
||||
//! }
|
||||
|
|
@ -387,7 +387,7 @@
|
|||
//! # pub fn update(&mut self, message: Message) -> Action { unimplemented!() }
|
||||
//! # pub fn view(&self) -> Element<Message> { unimplemented!() }
|
||||
//! # }
|
||||
//! # #[derive(Debug)]
|
||||
//! # #[derive(Debug, Clone)]
|
||||
//! # pub enum Message {}
|
||||
//! # pub enum Action { None, Run(Task<Message>), Chat(()) }
|
||||
//! # }
|
||||
|
|
@ -399,7 +399,7 @@
|
|||
//! # pub fn update(&mut self, message: Message) -> Task<Message> { unimplemented!() }
|
||||
//! # pub fn view(&self) -> Element<Message> { unimplemented!() }
|
||||
//! # }
|
||||
//! # #[derive(Debug)]
|
||||
//! # #[derive(Debug, Clone)]
|
||||
//! # pub enum Message {}
|
||||
//! # }
|
||||
//! use contacts::Contacts;
|
||||
|
|
@ -535,7 +535,7 @@ pub use alignment::Vertical::{Bottom, Top};
|
|||
|
||||
pub mod debug {
|
||||
//! Debug your applications.
|
||||
pub use iced_debug::{Span, skip_next_timing, time, time_with};
|
||||
pub use iced_debug::{Span, time, time_with};
|
||||
}
|
||||
|
||||
pub mod task {
|
||||
|
|
@ -697,7 +697,7 @@ pub fn run<State, Message, Theme, Renderer>(
|
|||
) -> Result
|
||||
where
|
||||
State: Default + 'static,
|
||||
Message: std::fmt::Debug + Send + 'static,
|
||||
Message: program::Message + 'static,
|
||||
Theme: Default + theme::Base + 'static,
|
||||
Renderer: program::Renderer + 'static,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@ impl core::text::Renderer for Renderer {
|
|||
type Paragraph = Paragraph;
|
||||
type Editor = Editor;
|
||||
|
||||
const MONOSPACE_FONT: Font = Font::MONOSPACE;
|
||||
const ICON_FONT: Font = Font::with_name("Iced-Icons");
|
||||
const CHECKMARK_ICON: char = '\u{f00c}';
|
||||
const ARROW_DOWN_ICON: char = '\u{e800}';
|
||||
|
|
|
|||
|
|
@ -643,6 +643,7 @@ impl core::text::Renderer for Renderer {
|
|||
type Paragraph = Paragraph;
|
||||
type Editor = Editor;
|
||||
|
||||
const MONOSPACE_FONT: Font = Font::MONOSPACE;
|
||||
const ICON_FONT: Font = Font::with_name("Iced-Icons");
|
||||
const CHECKMARK_ICON: char = '\u{f00c}';
|
||||
const ARROW_DOWN_ICON: char = '\u{e800}';
|
||||
|
|
|
|||
|
|
@ -1069,10 +1069,7 @@ fn update<P: Program, E: Executor>(
|
|||
P::Theme: theme::Base,
|
||||
{
|
||||
for message in messages.drain(..) {
|
||||
let update_span = debug::update(&message);
|
||||
let task = runtime.enter(|| program.update(message));
|
||||
debug::tasks_spawned(task.units());
|
||||
update_span.finish();
|
||||
|
||||
if let Some(stream) = runtime::task::into_stream(task) {
|
||||
runtime.run(stream);
|
||||
|
|
@ -1082,7 +1079,6 @@ fn update<P: Program, E: Executor>(
|
|||
let subscription = runtime.enter(|| program.subscription());
|
||||
let recipes = subscription::into_recipes(subscription.map(Action::Output));
|
||||
|
||||
debug::subscriptions_tracked(recipes.len());
|
||||
runtime.track(recipes);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue