use std::rc::Rc;
use gpui::{
App, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
prelude::FluentBuilder, px,
};
use gpui_kit_assets::{Icon, icon};
use gpui_kit_semantics::{NodeSpec, Role, Semantic};
use gpui_kit_theme::{ActiveTheme, ControlSize, Radius, Space, TypeScale};
use crate::controls::button::Button;
use crate::foundation::{Ident, Sizable, StyledExt};
use crate::strings::{ActiveStrings, StringKey};
type RetryHandler = Rc<dyn Fn(&mut Window, &mut App)>;
#[derive(IntoElement)]
pub struct FailurePanel {
ident: Ident,
title: Option<SharedString>,
reason: SharedString,
detail: Option<SharedString>,
attempts: Option<usize>,
retrying: bool,
on_retry: Option<RetryHandler>,
}
impl std::fmt::Debug for FailurePanel {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("FailurePanel")
.field("ident", &self.ident)
.field("title", &self.title)
.field("reason", &self.reason)
.field("attempts", &self.attempts)
.field("retrying", &self.retrying)
.field("has_handler", &self.on_retry.is_some())
.finish()
}
}
impl FailurePanel {
pub fn new(ident: impl Into<Ident>, reason: impl Into<SharedString>) -> Self {
Self {
ident: ident.into(),
title: None,
reason: reason.into(),
detail: None,
attempts: None,
retrying: false,
on_retry: None,
}
}
pub fn from_result<T, E: std::fmt::Display>(
ident: impl Into<Ident>,
result: &Result<T, E>,
) -> Option<Self> {
match result {
Ok(_) => None,
Err(error) => Some(Self::new(ident, error.to_string())),
}
}
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
self.title = Some(title.into());
self
}
pub fn detail(mut self, detail: impl Into<SharedString>) -> Self {
self.detail = Some(detail.into());
self
}
pub fn attempts(mut self, attempts: usize) -> Self {
self.attempts = Some(attempts);
self
}
pub fn retrying(mut self, retrying: bool) -> Self {
self.retrying = retrying;
self
}
pub fn on_retry(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_retry = Some(Rc::new(handler));
self
}
}
impl RenderOnce for FailurePanel {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().clone();
let title = self
.title
.clone()
.unwrap_or_else(|| cx.strings().text(StringKey::FailureTitle));
let retry = self.on_retry.clone().map(|handler| {
Button::new(self.ident.child("retry"))
.label(cx.strings().text(StringKey::TryAgain))
.secondary()
.control_size(ControlSize::Sm)
.semantic_parent(self.ident.semantic_id())
.loading(self.retrying)
.on_click(move |window, cx| handler(window, cx))
});
let attempts = self.attempts.filter(|count| *count > 1).map(|count| {
let wording = cx
.strings()
.format(StringKey::FailureAttempts, &[&count.to_string()]);
div()
.type_scale(&theme, TypeScale::Caption)
.text_color(theme.colors.text_faint)
.child(wording.clone())
.semantic_in(
cx,
NodeSpec::new(self.ident.child("attempts").semantic_id(), Role::Text)
.parent(self.ident.semantic_id())
.text(wording)
.value(count.to_string()),
)
});
let status = self.retrying.then(|| {
let wording = cx.strings().text(StringKey::FailureRetrying);
div()
.type_scale(&theme, TypeScale::Caption)
.text_color(theme.colors.text_muted)
.child(wording.clone())
.semantic_in(
cx,
NodeSpec::new(self.ident.child("retrying").semantic_id(), Role::Status)
.parent(self.ident.semantic_id())
.text(wording)
.busy(true),
)
});
let reason_ident = self.ident.child("reason");
div()
.column()
.w_full()
.gap_token(&theme, Space::Sm)
.p_token(&theme, Space::Lg)
.radius(&theme, Radius::Card)
.bg(theme.colors.panel)
.glow(&theme, theme.colors.danger)
.child(
div()
.row()
.gap_token(&theme, Space::Sm)
.child(
icon(Icon::Danger)
.size(px(theme.control.md.icon_size))
.text_color(theme.colors.danger),
)
.child(
div()
.type_scale(&theme, TypeScale::Label)
.text_color(theme.colors.text)
.child(title.clone()),
),
)
.child(
div()
.type_scale(&theme, TypeScale::Body)
.text_color(theme.colors.text_muted)
.child(self.reason.clone())
.semantic_in(
cx,
NodeSpec::new(reason_ident.semantic_id(), Role::Text)
.parent(self.ident.semantic_id())
.text(self.reason.clone()),
),
)
.when_some(self.detail.clone(), |element, detail| {
element.child(
div()
.type_scale(&theme, TypeScale::Caption)
.text_color(theme.colors.text_faint)
.child(detail),
)
})
.children(attempts)
.children(status)
.children(retry.map(|control| div().row().child(control)))
.semantic_in(
cx,
NodeSpec::new(self.ident.semantic_id(), Role::Region)
.text(title)
.value("failed")
.invalid(true)
.busy(self.retrying),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_held_value_produces_no_panel() {
let held: Result<u8, String> = Ok(7);
assert!(FailurePanel::from_result("panel", &held).is_none());
}
#[test]
fn a_held_failure_carries_the_hosts_own_words() {
let held: Result<u8, String> = Err("the index is still building".into());
let panel = FailurePanel::from_result("panel", &held).expect("a failure panel");
assert_eq!(panel.reason, "the index is still building");
}
}