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 Editor = ();
const MONOSPACE_FONT: Font = Font::MONOSPACE;
const ICON_FONT: Font = Font::DEFAULT;
const CHECKMARK_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`].
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;

View file

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

View file

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

View file

@ -10,11 +10,12 @@ 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;
@ -124,9 +125,9 @@ where
enum Message {
HideNotification,
ToggleComet,
CometLaunched(Result<(), comet::Error>),
CometLaunched(comet::launch::Result),
InstallComet,
InstallationProgressed(Result<comet::Installation, comet::Error>),
Installing(comet::install::Result),
CancelSetup,
}
@ -195,17 +196,17 @@ where
Message::CometLaunched(Ok(())) => Task::none(),
Message::CometLaunched(Err(error)) => {
match error {
comet::Error::NotFound => {
comet::launch::Error::NotFound => {
self.mode = Mode::Setup(Setup::Idle {
goal: Goal::Installation,
});
}
comet::Error::Outdated { revision } => {
comet::launch::Error::Outdated { revision } => {
self.mode = Mode::Setup(Setup::Idle {
goal: Goal::Update { revision },
});
}
comet::Error::IoFailed(error) => {
comet::launch::Error::IoFailed(error) => {
log::error!("comet failed to run: {error}");
}
}
@ -217,29 +218,42 @@ where
Mode::Setup(Setup::Running { logs: Vec::new() });
comet::install()
.map(Message::InstallationProgressed)
.map(Message::Installing)
.map(Event::Message)
}
Message::InstallationProgressed(Ok(installation)) => {
Message::Installing(Ok(installation)) => {
let Mode::Setup(Setup::Running { logs }) = &mut self.mode
else {
return Task::none();
};
match installation {
comet::Installation::Logged(log) => {
comet::install::Event::Logged(log) => {
logs.push(log);
Task::none()
}
comet::Installation::Finished => {
comet::install::Event::Finished => {
self.mode = Mode::None;
comet::launch().discard()
}
}
}
Message::InstallationProgressed(_error) => {
// TODO
Message::Installing(Err(error)) => {
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()
}
Message::CancelSetup => {
@ -304,7 +318,7 @@ where
let derive_theme = move || {
theme
.palette()
.map(|palette| Theme::custom("DevTools".to_owned(), palette))
.map(|palette| Theme::custom("iced devtools", palette))
.unwrap_or_default()
};
@ -312,97 +326,14 @@ where
Mode::None => None,
Mode::Setup(setup) => {
let stage: Element<'_, _, Theme, P::Renderer> = match setup {
Setup::Idle { goal } => {
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",
)
.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(),
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)
@ -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 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;

View file

@ -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}';

View file

@ -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}';