Draft time-travel debugging feature
This commit is contained in:
parent
388a419ed5
commit
d5d4479a53
20 changed files with 330 additions and 63 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2453,6 +2453,7 @@ version = "0.14.0-dev"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iced_beacon",
|
"iced_beacon",
|
||||||
"iced_core",
|
"iced_core",
|
||||||
|
"iced_futures",
|
||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ markdown = ["iced_widget/markdown"]
|
||||||
lazy = ["iced_widget/lazy"]
|
lazy = ["iced_widget/lazy"]
|
||||||
# Enables a debug view in native platforms (press F12)
|
# Enables a debug view in native platforms (press F12)
|
||||||
debug = ["iced_winit/debug", "iced_devtools"]
|
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
|
# 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
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
|
use crate::Error;
|
||||||
use crate::core::time::{Duration, SystemTime};
|
use crate::core::time::{Duration, SystemTime};
|
||||||
use crate::span;
|
use crate::span;
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
|
use futures::{FutureExt, select};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io::{self, AsyncWriteExt};
|
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net;
|
use tokio::net;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
|
|
@ -17,7 +19,7 @@ pub const SERVER_ADDRESS: &str = "127.0.0.1:9167";
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
sender: mpsc::Sender<Message>,
|
sender: mpsc::Sender<Action>,
|
||||||
is_connected: Arc<AtomicBool>,
|
is_connected: Arc<AtomicBool>,
|
||||||
_handle: Arc<thread::JoinHandle<()>>,
|
_handle: Arc<thread::JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
@ -43,17 +45,17 @@ pub enum Event {
|
||||||
ThemeChanged(theme::Palette),
|
ThemeChanged(theme::Palette),
|
||||||
SpanStarted(span::Stage),
|
SpanStarted(span::Stage),
|
||||||
SpanFinished(span::Stage, Duration),
|
SpanFinished(span::Stage, Duration),
|
||||||
MessageLogged(String),
|
MessageLogged { number: usize, message: String },
|
||||||
CommandsSpawned(usize),
|
CommandsSpawned(usize),
|
||||||
SubscriptionsTracked(usize),
|
SubscriptionsTracked(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub fn log(&self, event: Event) {
|
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(),
|
at: SystemTime::now(),
|
||||||
event,
|
event,
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_connected(&self) -> bool {
|
pub fn is_connected(&self) -> bool {
|
||||||
|
|
@ -61,21 +63,28 @@ impl Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn quit(&self) {
|
pub fn quit(&self) {
|
||||||
let _ = self.sender.try_send(Message::Quit {
|
let _ = self.sender.try_send(Action::Send(Message::Quit {
|
||||||
at: SystemTime::now(),
|
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]
|
#[must_use]
|
||||||
pub fn connect(name: String) -> Client {
|
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 is_connected = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
let handle = {
|
let handle = {
|
||||||
let is_connected = is_connected.clone();
|
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 {
|
Client {
|
||||||
|
|
@ -85,16 +94,30 @@ pub fn connect(name: String) -> Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Action {
|
||||||
|
Send(Message),
|
||||||
|
Forward(mpsc::Sender<Command>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum Command {
|
||||||
|
RewindTo { message: usize },
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn run(
|
async fn run(
|
||||||
name: String,
|
name: String,
|
||||||
is_connected: Arc<AtomicBool>,
|
is_connected: Arc<AtomicBool>,
|
||||||
mut receiver: mpsc::Receiver<Message>,
|
mut receiver: mpsc::Receiver<Action>,
|
||||||
) {
|
) {
|
||||||
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
|
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
|
||||||
.expect("Parse package version");
|
.expect("Parse package version");
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
let mut command_sender = None;
|
||||||
|
|
||||||
match _connect().await {
|
match _connect().await {
|
||||||
Ok(mut stream) => {
|
Ok(mut stream) => {
|
||||||
is_connected.store(true, atomic::Ordering::Relaxed);
|
is_connected.store(true, atomic::Ordering::Relaxed);
|
||||||
|
|
@ -109,16 +132,37 @@ async fn run(
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
while let Some(output) = receiver.recv().await {
|
loop {
|
||||||
match send(&mut stream, output).await {
|
select! {
|
||||||
Ok(()) => {}
|
action = receiver.recv().fuse() => {
|
||||||
Err(error) => {
|
let Some(action) = action else { break; };
|
||||||
if error.kind() != io::ErrorKind::BrokenPipe {
|
|
||||||
log::warn!(
|
match action {
|
||||||
"Error sending message to server: {error}"
|
Action::Send(message) => {
|
||||||
);
|
match send(&mut stream, message).await {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(error) => {
|
||||||
|
if error.kind() != io::ErrorKind::BrokenPipe
|
||||||
|
{
|
||||||
|
log::warn!(
|
||||||
|
"Error sending message to server: {error}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Forward(sender) => {
|
||||||
|
command_sender = Some(sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
command = receive(&mut stream, &mut buffer).fuse() => {
|
||||||
|
let Ok(command) = command else { continue; };
|
||||||
|
|
||||||
|
if let Some(sender) = command_sender.as_mut() {
|
||||||
|
let _ = sender.send(command).await;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,3 +198,18 @@ async fn send(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
stream: &mut net::TcpStream,
|
||||||
|
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 client;
|
||||||
pub mod span;
|
pub mod span;
|
||||||
|
|
||||||
|
mod error;
|
||||||
mod stream;
|
mod stream;
|
||||||
|
|
||||||
pub use client::Client;
|
pub use client::Client;
|
||||||
|
|
@ -11,14 +12,36 @@ pub use span::Span;
|
||||||
|
|
||||||
use crate::core::theme;
|
use crate::core::theme;
|
||||||
use crate::core::time::{Duration, SystemTime};
|
use crate::core::time::{Duration, SystemTime};
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
use futures::{SinkExt, Stream};
|
use futures::{SinkExt, Stream};
|
||||||
use tokio::io::{self, AsyncReadExt};
|
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Connected {
|
Connected {
|
||||||
|
connection: Connection,
|
||||||
at: SystemTime,
|
at: SystemTime,
|
||||||
name: String,
|
name: String,
|
||||||
version: Version,
|
version: Version,
|
||||||
|
|
@ -86,18 +109,42 @@ pub fn run() -> impl Stream<Item = Event> {
|
||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let Ok((mut stream, _)) = server.accept().await else {
|
let Ok((stream, _)) = server.accept().await else {
|
||||||
continue;
|
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_message = String::new();
|
||||||
|
let mut last_update_number = 0;
|
||||||
let mut last_commands_spawned = 0;
|
let mut last_commands_spawned = 0;
|
||||||
let mut last_present_window = None;
|
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 {
|
||||||
|
let client::Command::RewindTo { message } = command;
|
||||||
|
|
||||||
|
if Some(message) == last_message_number {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_message_number = Some(message);
|
||||||
|
|
||||||
|
let _ =
|
||||||
|
send(&mut writer, command).await.inspect_err(|error| {
|
||||||
|
log::error!("Error when sending command: {error}")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match receive(&mut stream, &mut buffer).await {
|
match receive(&mut reader, &mut buffer).await {
|
||||||
Ok(message) => {
|
Ok(message) => {
|
||||||
match message {
|
match message {
|
||||||
client::Message::Connected {
|
client::Message::Connected {
|
||||||
|
|
@ -107,6 +154,9 @@ pub fn run() -> impl Stream<Item = Event> {
|
||||||
} => {
|
} => {
|
||||||
let _ = output
|
let _ = output
|
||||||
.send(Event::Connected {
|
.send(Event::Connected {
|
||||||
|
connection: Connection {
|
||||||
|
commands: command_sender.clone(),
|
||||||
|
},
|
||||||
at,
|
at,
|
||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
|
|
@ -133,7 +183,11 @@ pub fn run() -> impl Stream<Item = Event> {
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
client::Event::MessageLogged(message) => {
|
client::Event::MessageLogged {
|
||||||
|
number,
|
||||||
|
message,
|
||||||
|
} => {
|
||||||
|
last_update_number = number;
|
||||||
last_message = message;
|
last_message = message;
|
||||||
}
|
}
|
||||||
client::Event::CommandsSpawned(
|
client::Event::CommandsSpawned(
|
||||||
|
|
@ -161,6 +215,7 @@ pub fn run() -> impl Stream<Item = Event> {
|
||||||
span::Stage::Boot => Span::Boot,
|
span::Stage::Boot => Span::Boot,
|
||||||
span::Stage::Update => {
|
span::Stage::Update => {
|
||||||
Span::Update {
|
Span::Update {
|
||||||
|
number: last_update_number,
|
||||||
message: last_message
|
message: last_message
|
||||||
.clone(),
|
.clone(),
|
||||||
commands_spawned:
|
commands_spawned:
|
||||||
|
|
@ -246,7 +301,7 @@ pub fn run() -> impl Stream<Item = Event> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn receive(
|
async fn receive(
|
||||||
stream: &mut net::TcpStream,
|
stream: &mut net::tcp::OwnedReadHalf,
|
||||||
buffer: &mut Vec<u8>,
|
buffer: &mut Vec<u8>,
|
||||||
) -> Result<client::Message, Error> {
|
) -> Result<client::Message, Error> {
|
||||||
let size = stream.read_u64().await? as usize;
|
let size = stream.read_u64().await? as usize;
|
||||||
|
|
@ -260,14 +315,20 @@ async fn receive(
|
||||||
Ok(bincode::deserialize(buffer)?)
|
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() {
|
async fn delay() {
|
||||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
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,6 +6,7 @@ use serde::{Deserialize, Serialize};
|
||||||
pub enum Span {
|
pub enum Span {
|
||||||
Boot,
|
Boot,
|
||||||
Update {
|
Update {
|
||||||
|
number: usize,
|
||||||
message: String,
|
message: String,
|
||||||
commands_spawned: usize,
|
commands_spawned: usize,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ enable = ["dep:iced_beacon"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced_core.workspace = true
|
iced_core.workspace = true
|
||||||
|
iced_futures.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
pub use iced_core as core;
|
pub use iced_core as core;
|
||||||
|
pub use iced_futures as futures;
|
||||||
|
|
||||||
use crate::core::theme;
|
use crate::core::theme;
|
||||||
use crate::core::window;
|
use crate::core::window;
|
||||||
|
use crate::futures::Subscription;
|
||||||
|
|
||||||
pub use internal::Span;
|
pub use internal::Span;
|
||||||
|
|
||||||
|
|
@ -16,6 +18,11 @@ pub enum Primitive {
|
||||||
Text,
|
Text,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Command {
|
||||||
|
RewindTo { message: usize },
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init(name: &str) {
|
pub fn init(name: &str) {
|
||||||
internal::init(name);
|
internal::init(name);
|
||||||
}
|
}
|
||||||
|
|
@ -88,12 +95,18 @@ pub fn skip_next_timing() {
|
||||||
internal::skip_next_timing();
|
internal::skip_next_timing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn commands() -> Subscription<Command> {
|
||||||
|
internal::commands()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "enable", not(target_arch = "wasm32")))]
|
#[cfg(all(feature = "enable", not(target_arch = "wasm32")))]
|
||||||
mod internal {
|
mod internal {
|
||||||
use crate::Primitive;
|
|
||||||
use crate::core::theme;
|
use crate::core::theme;
|
||||||
use crate::core::time::Instant;
|
use crate::core::time::Instant;
|
||||||
use crate::core::window;
|
use crate::core::window;
|
||||||
|
use crate::futures::Subscription;
|
||||||
|
use crate::futures::futures::Stream;
|
||||||
|
use crate::{Command, Primitive};
|
||||||
|
|
||||||
use iced_beacon as beacon;
|
use iced_beacon as beacon;
|
||||||
|
|
||||||
|
|
@ -102,7 +115,7 @@ mod internal {
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::process;
|
use std::process;
|
||||||
use std::sync::atomic::{self, AtomicBool};
|
use std::sync::atomic::{self, AtomicBool, AtomicUsize};
|
||||||
use std::sync::{LazyLock, RwLock};
|
use std::sync::{LazyLock, RwLock};
|
||||||
|
|
||||||
pub fn init(name: &str) {
|
pub fn init(name: &str) {
|
||||||
|
|
@ -162,6 +175,8 @@ mod internal {
|
||||||
pub fn update(message: &impl std::fmt::Debug) -> Span {
|
pub fn update(message: &impl std::fmt::Debug) -> Span {
|
||||||
let span = span(span::Stage::Update);
|
let span = span(span::Stage::Update);
|
||||||
|
|
||||||
|
let number = LAST_UPDATE.fetch_add(1, atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let message = format!("{message:?}");
|
let message = format!("{message:?}");
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
|
|
@ -172,11 +187,13 @@ mod internal {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
BEACON.log(client::Event::MessageLogged(if message.len() > 49 {
|
let message = if message.len() > 49 {
|
||||||
format!("{}...", &message[..49])
|
format!("{}...", &message[..49])
|
||||||
} else {
|
} else {
|
||||||
message
|
message
|
||||||
}));
|
};
|
||||||
|
|
||||||
|
BEACON.log(client::Event::MessageLogged { number, message });
|
||||||
|
|
||||||
span
|
span
|
||||||
}
|
}
|
||||||
|
|
@ -217,6 +234,24 @@ mod internal {
|
||||||
SKIP_NEXT_SPAN.store(true, atomic::Ordering::Relaxed);
|
SKIP_NEXT_SPAN.store(true, 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 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((command, receiver))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Subscription::run(listen_for_commands)
|
||||||
|
}
|
||||||
|
|
||||||
fn span(span: span::Stage) -> Span {
|
fn span(span: span::Stage) -> Span {
|
||||||
BEACON.log(client::Event::SpanStarted(span.clone()));
|
BEACON.log(client::Event::SpanStarted(span.clone()));
|
||||||
|
|
||||||
|
|
@ -260,15 +295,17 @@ mod internal {
|
||||||
});
|
});
|
||||||
|
|
||||||
static NAME: RwLock<String> = RwLock::new(String::new());
|
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 LAST_PALETTE: RwLock<Option<theme::Palette>> = RwLock::new(None);
|
||||||
static SKIP_NEXT_SPAN: AtomicBool = AtomicBool::new(false);
|
static SKIP_NEXT_SPAN: AtomicBool = AtomicBool::new(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(not(feature = "enable"), target_arch = "wasm32"))]
|
#[cfg(any(not(feature = "enable"), target_arch = "wasm32"))]
|
||||||
mod internal {
|
mod internal {
|
||||||
use crate::Primitive;
|
|
||||||
use crate::core::theme;
|
use crate::core::theme;
|
||||||
use crate::core::window;
|
use crate::core::window;
|
||||||
|
use crate::futures::Subscription;
|
||||||
|
use crate::{Command, Primitive};
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
|
|
@ -326,6 +363,10 @@ mod internal {
|
||||||
|
|
||||||
pub fn skip_next_timing() {}
|
pub fn skip_next_timing() {}
|
||||||
|
|
||||||
|
pub fn commands() -> Subscription<Command> {
|
||||||
|
Subscription::none()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Span;
|
pub struct Span;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ rust-version.workspace = true
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
time-travel = ["iced_program/time-travel"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced_program.workspace = true
|
iced_program.workspace = true
|
||||||
iced_widget.workspace = true
|
iced_widget.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,8 @@ where
|
||||||
state: P::State,
|
state: P::State,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
show_notification: bool,
|
show_notification: bool,
|
||||||
|
rewind: Option<P::State>,
|
||||||
|
log: Vec<P::Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -147,6 +149,8 @@ where
|
||||||
state,
|
state,
|
||||||
mode: Mode::None,
|
mode: Mode::None,
|
||||||
show_notification: true,
|
show_notification: true,
|
||||||
|
rewind: None,
|
||||||
|
log: Vec::new(),
|
||||||
},
|
},
|
||||||
executor::spawn_blocking(|mut sender| {
|
executor::spawn_blocking(|mut sender| {
|
||||||
thread::sleep(seconds(2));
|
thread::sleep(seconds(2));
|
||||||
|
|
@ -250,7 +254,46 @@ where
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Event::Program(message) => {
|
Event::Program(message) => {
|
||||||
program.update(&mut self.state, message).map(Event::Program)
|
if self.rewind.is_some() {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "time-travel")]
|
||||||
|
{
|
||||||
|
self.log.push(message.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let span = debug::update(&message);
|
||||||
|
let task = program.update(&mut self.state, message);
|
||||||
|
debug::tasks_spawned(task.units());
|
||||||
|
span.finish();
|
||||||
|
|
||||||
|
task.map(Event::Program)
|
||||||
|
}
|
||||||
|
Event::Command(command) => {
|
||||||
|
match command {
|
||||||
|
debug::Command::RewindTo { message } => {
|
||||||
|
#[cfg(feature = "time-travel")]
|
||||||
|
{
|
||||||
|
let (mut state, _) = program.boot();
|
||||||
|
|
||||||
|
if message < self.log.len() {
|
||||||
|
// TODO: Run concurrently (?)
|
||||||
|
for message in &self.log[0..message] {
|
||||||
|
let _ = program
|
||||||
|
.update(&mut state, message.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rewind = Some(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "time-travel"))]
|
||||||
|
let _ = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -260,8 +303,10 @@ where
|
||||||
program: &P,
|
program: &P,
|
||||||
window: window::Id,
|
window: window::Id,
|
||||||
) -> Element<'_, Event<P>, P::Theme, P::Renderer> {
|
) -> Element<'_, Event<P>, P::Theme, P::Renderer> {
|
||||||
let view = program.view(&self.state, window).map(Event::Program);
|
let state = self.rewind.as_ref().unwrap_or(&self.state);
|
||||||
let theme = program.theme(&self.state, window);
|
|
||||||
|
let view = program.view(state, window).map(Event::Program);
|
||||||
|
let theme = program.theme(state, window);
|
||||||
|
|
||||||
let derive_theme = move || {
|
let derive_theme = move || {
|
||||||
theme
|
theme
|
||||||
|
|
@ -363,6 +408,7 @@ where
|
||||||
});
|
});
|
||||||
|
|
||||||
stack![view]
|
stack![view]
|
||||||
|
.height(Fill)
|
||||||
.push_maybe(mode.map(opaque))
|
.push_maybe(mode.map(opaque))
|
||||||
.push_maybe(notification)
|
.push_maybe(notification)
|
||||||
.into()
|
.into()
|
||||||
|
|
@ -372,6 +418,8 @@ where
|
||||||
let subscription =
|
let subscription =
|
||||||
program.subscription(&self.state).map(Event::Program);
|
program.subscription(&self.state).map(Event::Program);
|
||||||
|
|
||||||
|
debug::subscriptions_tracked(subscription.units());
|
||||||
|
|
||||||
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) => {
|
||||||
|
|
@ -381,19 +429,22 @@ where
|
||||||
})
|
})
|
||||||
.map(Event::Message);
|
.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 {
|
pub fn theme(&self, program: &P, window: window::Id) -> P::Theme {
|
||||||
program.theme(&self.state, window)
|
program.theme(self.rewind.as_ref().unwrap_or(&self.state), window)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn style(&self, program: &P, theme: &P::Theme) -> theme::Style {
|
pub fn style(&self, program: &P, theme: &P::Theme) -> theme::Style {
|
||||||
program.style(&self.state, theme)
|
program.style(self.rewind.as_ref().unwrap_or(&self.state), theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scale_factor(&self, program: &P, window: window::Id) -> f64 {
|
pub fn scale_factor(&self, program: &P, window: window::Id) -> f64 {
|
||||||
program.scale_factor(&self.state, window)
|
program
|
||||||
|
.scale_factor(self.rewind.as_ref().unwrap_or(&self.state), window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -403,6 +454,7 @@ where
|
||||||
{
|
{
|
||||||
Message(Message),
|
Message(Message),
|
||||||
Program(P::Message),
|
Program(P::Message),
|
||||||
|
Command(debug::Command),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P> fmt::Debug for Event<P>
|
impl<P> fmt::Debug for Event<P>
|
||||||
|
|
@ -413,6 +465,21 @@ where
|
||||||
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::Command(command) => command.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "time-travel")]
|
||||||
|
impl<P> Clone for Event<P>
|
||||||
|
where
|
||||||
|
P: Program,
|
||||||
|
{
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Event::Message(message) => Event::Message(message.clone()),
|
||||||
|
Event::Program(message) => Event::Program(message.clone()),
|
||||||
|
Event::Command(command) => Event::Command(*command),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ struct Multitouch {
|
||||||
fingers: HashMap<touch::Finger, Point>,
|
fingers: HashMap<touch::Finger, Point>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
enum Message {
|
enum Message {
|
||||||
FingerPressed { id: touch::Finger, position: Point },
|
FingerPressed { id: touch::Finger, position: Point },
|
||||||
FingerLifted { id: touch::Finger },
|
FingerLifted { id: touch::Finger },
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,10 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, now: Instant) {
|
pub fn update(&mut self, now: Instant) {
|
||||||
|
if self.start > now {
|
||||||
|
self.start = now;
|
||||||
|
}
|
||||||
|
|
||||||
self.now = now;
|
self.now = now;
|
||||||
self.system_cache.clear();
|
self.system_cache.clear();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
iced.features = ["tokio", "debug"]
|
iced.features = ["tokio", "debug", "time-travel"]
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,11 @@ impl<T> Subscription<T> {
|
||||||
.collect(),
|
.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.
|
/// Creates a [`Subscription`] from a [`Recipe`] describing it.
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,12 @@ categories.workspace = true
|
||||||
keywords.workspace = true
|
keywords.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
time-travel = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced_graphics.workspace = true
|
iced_graphics.workspace = true
|
||||||
iced_runtime.workspace = true
|
iced_runtime.workspace = true
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ pub trait Program: Sized {
|
||||||
type State;
|
type State;
|
||||||
|
|
||||||
/// The message of the program.
|
/// The message of the program.
|
||||||
type Message: Send + std::fmt::Debug + 'static;
|
type Message: Message + 'static;
|
||||||
|
|
||||||
/// The theme of the program.
|
/// The theme of the program.
|
||||||
type Theme: Default + theme::Base;
|
type Theme: Default + theme::Base;
|
||||||
|
|
@ -642,3 +642,17 @@ 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 {}
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,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: Send + std::fmt::Debug + 'static,
|
Message: program::Message + 'static,
|
||||||
Theme: Default + theme::Base,
|
Theme: Default + theme::Base,
|
||||||
Renderer: program::Renderer,
|
Renderer: program::Renderer,
|
||||||
{
|
{
|
||||||
|
|
@ -94,7 +94,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: Send + std::fmt::Debug + 'static,
|
Message: program::Message + '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>,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,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: Send + std::fmt::Debug + 'static,
|
Message: program::Message + 'static,
|
||||||
Theme: Default + theme::Base,
|
Theme: Default + theme::Base,
|
||||||
Renderer: program::Renderer,
|
Renderer: program::Renderer,
|
||||||
{
|
{
|
||||||
|
|
@ -44,7 +44,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: Send + std::fmt::Debug + 'static,
|
Message: program::Message + '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>,
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,7 @@
|
||||||
//! use iced::window;
|
//! use iced::window;
|
||||||
//! use iced::{Size, Subscription};
|
//! use iced::{Size, Subscription};
|
||||||
//!
|
//!
|
||||||
//! #[derive(Debug)]
|
//! #[derive(Debug, Clone)]
|
||||||
//! enum Message {
|
//! enum Message {
|
||||||
//! WindowResized(Size),
|
//! WindowResized(Size),
|
||||||
//! }
|
//! }
|
||||||
|
|
@ -387,7 +387,7 @@
|
||||||
//! # pub fn update(&mut self, message: Message) -> Action { unimplemented!() }
|
//! # pub fn update(&mut self, message: Message) -> Action { unimplemented!() }
|
||||||
//! # pub fn view(&self) -> Element<Message> { unimplemented!() }
|
//! # pub fn view(&self) -> Element<Message> { unimplemented!() }
|
||||||
//! # }
|
//! # }
|
||||||
//! # #[derive(Debug)]
|
//! # #[derive(Debug, Clone)]
|
||||||
//! # pub enum Message {}
|
//! # pub enum Message {}
|
||||||
//! # pub enum Action { None, Run(Task<Message>), Chat(()) }
|
//! # pub enum Action { None, Run(Task<Message>), Chat(()) }
|
||||||
//! # }
|
//! # }
|
||||||
|
|
@ -399,7 +399,7 @@
|
||||||
//! # pub fn update(&mut self, message: Message) -> Task<Message> { unimplemented!() }
|
//! # pub fn update(&mut self, message: Message) -> Task<Message> { unimplemented!() }
|
||||||
//! # pub fn view(&self) -> Element<Message> { unimplemented!() }
|
//! # pub fn view(&self) -> Element<Message> { unimplemented!() }
|
||||||
//! # }
|
//! # }
|
||||||
//! # #[derive(Debug)]
|
//! # #[derive(Debug, Clone)]
|
||||||
//! # pub enum Message {}
|
//! # pub enum Message {}
|
||||||
//! # }
|
//! # }
|
||||||
//! use contacts::Contacts;
|
//! use contacts::Contacts;
|
||||||
|
|
@ -697,7 +697,7 @@ pub fn run<State, Message, Theme, Renderer>(
|
||||||
) -> Result
|
) -> Result
|
||||||
where
|
where
|
||||||
State: Default + 'static,
|
State: Default + 'static,
|
||||||
Message: std::fmt::Debug + Send + 'static,
|
Message: program::Message + 'static,
|
||||||
Theme: Default + theme::Base + 'static,
|
Theme: Default + theme::Base + 'static,
|
||||||
Renderer: program::Renderer + 'static,
|
Renderer: program::Renderer + 'static,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1069,10 +1069,7 @@ fn update<P: Program, E: Executor>(
|
||||||
P::Theme: theme::Base,
|
P::Theme: theme::Base,
|
||||||
{
|
{
|
||||||
for message in messages.drain(..) {
|
for message in messages.drain(..) {
|
||||||
let update_span = debug::update(&message);
|
|
||||||
let task = runtime.enter(|| program.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) {
|
if let Some(stream) = runtime::task::into_stream(task) {
|
||||||
runtime.run(stream);
|
runtime.run(stream);
|
||||||
|
|
@ -1082,7 +1079,6 @@ fn update<P: Program, E: Executor>(
|
||||||
let subscription = runtime.enter(|| program.subscription());
|
let subscription = runtime.enter(|| program.subscription());
|
||||||
let recipes = subscription::into_recipes(subscription.map(Action::Output));
|
let recipes = subscription::into_recipes(subscription.map(Action::Output));
|
||||||
|
|
||||||
debug::subscriptions_tracked(recipes.len());
|
|
||||||
runtime.track(recipes);
|
runtime.track(recipes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue