Tweak and organize devtools crate

This commit is contained in:
Héctor Ramón Jiménez 2025-04-28 22:31:13 +02:00
parent 267583c2a9
commit ef16ea3b2a
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
8 changed files with 239 additions and 132 deletions

View file

@ -31,6 +31,7 @@ impl text::Renderer for () {
type Paragraph = (); type Paragraph = ();
type Editor = (); type Editor = ();
const MONOSPACE_FONT: Font = Font::MONOSPACE;
const ICON_FONT: Font = Font::DEFAULT; const ICON_FONT: Font = Font::DEFAULT;
const CHECKMARK_ICON: char = '0'; const CHECKMARK_ICON: char = '0';
const ARROW_DOWN_ICON: char = '0'; const ARROW_DOWN_ICON: char = '0';

View file

@ -232,6 +232,11 @@ pub trait Renderer: crate::Renderer {
/// The [`Editor`] of this [`Renderer`]. /// The [`Editor`] of this [`Renderer`].
type Editor: Editor<Font = Self::Font> + 'static; 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. /// The icon font of the backend.
const ICON_FONT: Self::Font; const ICON_FONT: Self::Font;

View file

@ -5,6 +5,7 @@ pub use palette::Palette;
use crate::Color; use crate::Color;
use std::borrow::Cow;
use std::fmt; use std::fmt;
use std::sync::Arc; use std::sync::Arc;
@ -87,14 +88,17 @@ impl Theme {
]; ];
/// Creates a new custom [`Theme`] from the given [`Palette`]. /// 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) Self::custom_with_fn(name, palette, palette::Extended::generate)
} }
/// Creates a new custom [`Theme`] from the given [`Palette`], with /// Creates a new custom [`Theme`] from the given [`Palette`], with
/// a custom generator of a [`palette::Extended`]. /// a custom generator of a [`palette::Extended`].
pub fn custom_with_fn( pub fn custom_with_fn(
name: String, name: impl Into<Cow<'static, str>>,
palette: Palette, palette: Palette,
generate: impl FnOnce(Palette) -> palette::Extended, generate: impl FnOnce(Palette) -> palette::Extended,
) -> Self { ) -> Self {
@ -220,7 +224,7 @@ impl fmt::Display for Theme {
/// A [`Theme`] with a customized [`Palette`]. /// A [`Theme`] with a customized [`Palette`].
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Custom { pub struct Custom {
name: String, name: Cow<'static, str>,
palette: Palette, palette: Palette,
extended: palette::Extended, extended: palette::Extended,
} }
@ -234,12 +238,12 @@ impl Custom {
/// Creates a [`Custom`] theme from the given [`Palette`] with /// Creates a [`Custom`] theme from the given [`Palette`] with
/// a custom generator of a [`palette::Extended`]. /// a custom generator of a [`palette::Extended`].
pub fn with_fn( pub fn with_fn(
name: String, name: impl Into<Cow<'static, str>>,
palette: Palette, palette: Palette,
generate: impl FnOnce(Palette) -> palette::Extended, generate: impl FnOnce(Palette) -> palette::Extended,
) -> Self { ) -> Self {
Self { Self {
name, name: name.into(),
palette, palette,
extended: generate(palette), extended: generate(palette),
} }

View file

@ -1,14 +1,12 @@
use crate::executor; use crate::executor;
use crate::runtime::Task; use crate::runtime::Task;
use std::io;
use std::process; use std::process;
use std::sync::Arc;
pub const COMPATIBLE_REVISION: &str = pub const COMPATIBLE_REVISION: &str =
"69dd2283886dccdaa1ee6e1c274af62f7250bc38"; "69dd2283886dccdaa1ee6e1c274af62f7250bc38";
pub fn launch() -> Task<Result<(), Error>> { pub fn launch() -> Task<launch::Result> {
executor::try_spawn_blocking(|mut sender| { executor::try_spawn_blocking(|mut sender| {
let cargo_install = process::Command::new("cargo") let cargo_install = process::Command::new("cargo")
.args(["install", "--list"]) .args(["install", "--list"])
@ -22,15 +20,15 @@ pub fn launch() -> Task<Result<(), Error>> {
} }
let Some((_, revision)) = line.rsplit_once("?rev=") else { let Some((_, revision)) = line.rsplit_once("?rev=") else {
return Err(Error::Outdated { revision: None }); return Err(launch::Error::Outdated { revision: None });
}; };
let Some((revision, _)) = revision.rsplit_once("#") else { let Some((revision, _)) = revision.rsplit_once("#") else {
return Err(Error::Outdated { revision: None }); return Err(launch::Error::Outdated { revision: None });
}; };
if revision != COMPATIBLE_REVISION { if revision != COMPATIBLE_REVISION {
return Err(Error::Outdated { return Err(launch::Error::Outdated {
revision: Some(revision.to_owned()), revision: Some(revision.to_owned()),
}); });
} }
@ -45,16 +43,16 @@ pub fn launch() -> Task<Result<(), Error>> {
return Ok(()); return Ok(());
} }
Err(Error::NotFound) Err(launch::Error::NotFound)
}) })
} }
pub fn install() -> Task<Result<Installation, Error>> { pub fn install() -> Task<install::Result> {
executor::try_spawn_blocking(|mut sender| { executor::try_spawn_blocking(|mut sender| {
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
let install = Command::new("cargo") let mut install = Command::new("cargo")
.args([ .args([
"install", "install",
"--locked", "--locked",
@ -68,41 +66,75 @@ pub fn install() -> Task<Result<Installation, Error>> {
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn()?; .spawn()?;
let mut stderr = let mut stderr = BufReader::new(
BufReader::new(install.stderr.expect("stderr must be piped")); install.stderr.take().expect("stderr must be piped"),
);
let mut log = String::new(); let mut log = String::new();
while let Ok(n) = stderr.read_line(&mut log) { while let Ok(n) = stderr.read_line(&mut log) {
if n == 0 { if n == 0 {
break; let status = install.wait()?;
if status.success() {
break;
} else {
return Err(install::Error::ProcessFailed(status));
}
} }
let _ = sender.try_send(Installation::Logged(log.clone())); let _ = sender.try_send(install::Event::Logged(log.clone()));
log.clear(); log.clear();
} }
let _ = sender.try_send(Installation::Finished); let _ = sender.try_send(install::Event::Finished);
Ok(()) Ok(())
}) })
} }
#[derive(Debug, Clone)] pub mod launch {
pub enum Installation { use std::io;
Logged(String), use std::sync::Arc;
Finished,
}
#[derive(Debug, Clone)] pub type Result = std::result::Result<(), Error>;
pub enum Error {
NotFound,
Outdated { revision: Option<String> },
IoFailed(Arc<io::Error>),
}
impl From<io::Error> for Error { #[derive(Debug, Clone)]
fn from(error: io::Error) -> Self { pub enum Error {
Self::IoFailed(Arc::new(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))
}
} }
} }

View file

@ -10,11 +10,12 @@ mod comet;
mod executor; mod executor;
mod time_machine; mod time_machine;
use crate::core::border;
use crate::core::keyboard; use crate::core::keyboard;
use crate::core::theme::{self, Base, Theme}; use crate::core::theme::{self, Base, Theme};
use crate::core::time::seconds; use crate::core::time::seconds;
use crate::core::window; use crate::core::window;
use crate::core::{Color, Element, Length::Fill}; use crate::core::{Alignment::Center, Color, Element, Length::Fill};
use crate::futures::Subscription; use crate::futures::Subscription;
use crate::program::Program; use crate::program::Program;
use crate::runtime::Task; use crate::runtime::Task;
@ -124,9 +125,9 @@ where
enum Message { enum Message {
HideNotification, HideNotification,
ToggleComet, ToggleComet,
CometLaunched(Result<(), comet::Error>), CometLaunched(comet::launch::Result),
InstallComet, InstallComet,
InstallationProgressed(Result<comet::Installation, comet::Error>), Installing(comet::install::Result),
CancelSetup, CancelSetup,
} }
@ -195,17 +196,17 @@ where
Message::CometLaunched(Ok(())) => Task::none(), Message::CometLaunched(Ok(())) => Task::none(),
Message::CometLaunched(Err(error)) => { Message::CometLaunched(Err(error)) => {
match error { match error {
comet::Error::NotFound => { comet::launch::Error::NotFound => {
self.mode = Mode::Setup(Setup::Idle { self.mode = Mode::Setup(Setup::Idle {
goal: Goal::Installation, goal: Goal::Installation,
}); });
} }
comet::Error::Outdated { revision } => { comet::launch::Error::Outdated { revision } => {
self.mode = Mode::Setup(Setup::Idle { self.mode = Mode::Setup(Setup::Idle {
goal: Goal::Update { revision }, goal: Goal::Update { revision },
}); });
} }
comet::Error::IoFailed(error) => { comet::launch::Error::IoFailed(error) => {
log::error!("comet failed to run: {error}"); log::error!("comet failed to run: {error}");
} }
} }
@ -217,29 +218,42 @@ where
Mode::Setup(Setup::Running { logs: Vec::new() }); Mode::Setup(Setup::Running { logs: Vec::new() });
comet::install() comet::install()
.map(Message::InstallationProgressed) .map(Message::Installing)
.map(Event::Message) .map(Event::Message)
} }
Message::InstallationProgressed(Ok(installation)) => { Message::Installing(Ok(installation)) => {
let Mode::Setup(Setup::Running { logs }) = &mut self.mode let Mode::Setup(Setup::Running { logs }) = &mut self.mode
else { else {
return Task::none(); return Task::none();
}; };
match installation { match installation {
comet::Installation::Logged(log) => { comet::install::Event::Logged(log) => {
logs.push(log); logs.push(log);
Task::none() Task::none()
} }
comet::Installation::Finished => { comet::install::Event::Finished => {
self.mode = Mode::None; self.mode = Mode::None;
comet::launch().discard() comet::launch().discard()
} }
} }
} }
Message::InstallationProgressed(_error) => { Message::Installing(Err(error)) => {
// TODO let Mode::Setup(Setup::Running { logs }) = &mut self.mode
else {
return Task::none();
};
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() Task::none()
} }
Message::CancelSetup => { Message::CancelSetup => {
@ -304,7 +318,7 @@ where
let derive_theme = move || { let derive_theme = move || {
theme theme
.palette() .palette()
.map(|palette| Theme::custom("DevTools".to_owned(), palette)) .map(|palette| Theme::custom("iced devtools", palette))
.unwrap_or_default() .unwrap_or_default()
}; };
@ -312,97 +326,14 @@ where
Mode::None => None, Mode::None => None,
Mode::Setup(setup) => { Mode::Setup(setup) => {
let stage: Element<'_, _, Theme, P::Renderer> = match setup { let stage: Element<'_, _, Theme, P::Renderer> = match setup {
Setup::Idle { goal } => { Setup::Idle { goal } => self::setup(goal),
let controls = row![ Setup::Running { logs } => installation(logs),
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",
)
.size(14),
)
.width(Fill)
.padding(5)
.style(container::dark);
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.",
"Do you wish to install it with the \
following command?",
command,
controls,
]
.spacing(20),
Goal::Update { revision } => column![
text("comet is out of date!").size(20),
text!(
"The installed revision is \"{current}\", \
but the latest compatible is \"{compatible}\".",
current = revision
.as_deref()
.unwrap_or("Unknown"),
compatible = comet::COMPATIBLE_REVISION,
),
"Do you wish to update it with the following \
command?",
command,
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(),
}; };
let setup = center( let setup = center(
container(stage) container(stage)
.padding(20) .padding(20)
.width(500) .max_width(500)
.style(container::bordered_box), .style(container::bordered_box),
) )
.padding(10) .padding(10)
@ -510,3 +441,134 @@ where
} }
} }
} }
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()
}

View file

@ -84,6 +84,7 @@ where
type Paragraph = A::Paragraph; type Paragraph = A::Paragraph;
type Editor = A::Editor; type Editor = A::Editor;
const MONOSPACE_FONT: Self::Font = A::MONOSPACE_FONT;
const ICON_FONT: Self::Font = A::ICON_FONT; const ICON_FONT: Self::Font = A::ICON_FONT;
const CHECKMARK_ICON: char = A::CHECKMARK_ICON; const CHECKMARK_ICON: char = A::CHECKMARK_ICON;
const ARROW_DOWN_ICON: char = A::ARROW_DOWN_ICON; const ARROW_DOWN_ICON: char = A::ARROW_DOWN_ICON;

View file

@ -235,6 +235,7 @@ impl core::text::Renderer for Renderer {
type Paragraph = Paragraph; type Paragraph = Paragraph;
type Editor = Editor; type Editor = Editor;
const MONOSPACE_FONT: Font = Font::MONOSPACE;
const ICON_FONT: Font = Font::with_name("Iced-Icons"); const ICON_FONT: Font = Font::with_name("Iced-Icons");
const CHECKMARK_ICON: char = '\u{f00c}'; const CHECKMARK_ICON: char = '\u{f00c}';
const ARROW_DOWN_ICON: char = '\u{e800}'; const ARROW_DOWN_ICON: char = '\u{e800}';

View file

@ -643,6 +643,7 @@ impl core::text::Renderer for Renderer {
type Paragraph = Paragraph; type Paragraph = Paragraph;
type Editor = Editor; type Editor = Editor;
const MONOSPACE_FONT: Font = Font::MONOSPACE;
const ICON_FONT: Font = Font::with_name("Iced-Icons"); const ICON_FONT: Font = Font::with_name("Iced-Icons");
const CHECKMARK_ICON: char = '\u{f00c}'; const CHECKMARK_ICON: char = '\u{f00c}';
const ARROW_DOWN_ICON: char = '\u{e800}'; const ARROW_DOWN_ICON: char = '\u{e800}';