#![expect(clippy::missing_assert_message, reason = "Deferred: Noisy")]
use std::sync::Arc;
use masonry::properties::types::{AsUnit, Length, UnitPoint};
use masonry::properties::{LineBreaking, Padding};
use tokio::sync::mpsc::UnboundedSender;
use vello::peniko::{Blob, ImageAlphaType, ImageData, ImageFormat};
use winit::dpi::LogicalSize;
use winit::error::EventLoopError;
use xilem::core::fork;
use xilem::core::one_of::OneOf3;
use xilem::style::Style as _;
use xilem::view::{
FlexSpacer, ZStackExt, flex_col, flex_row, image, inline_prose, portal, prose, sized_box,
spinner, split, text_button, worker, zstack,
};
use xilem::{EventLoop, EventLoopBuilder, TextAlign, WidgetView, WindowOptions, Xilem, palette};
struct HttpCats {
statuses: Vec<Status>,
selected_code: Option<u32>,
download_sender: Option<UnboundedSender<u32>>,
}
#[derive(Debug)]
struct Status {
code: u32,
message: &'static str,
image: ImageState,
}
#[derive(Debug)]
enum ImageState {
NotRequested,
Pending,
Available(ImageData),
}
impl HttpCats {
fn view(&mut self) -> impl WidgetView<Self> + use<> {
let left_column = portal(
flex_col((
prose("Status"),
self.statuses
.iter_mut()
.map(Status::list_view)
.collect::<Vec<_>>(),
))
.padding(Padding::left(5.)),
);
let info_area = if let Some(selected_code) = self.selected_code {
if let Some(selected_status) =
self.statuses.iter_mut().find(|it| it.code == selected_code)
{
OneOf3::A(selected_status.details_view())
} else {
OneOf3::B(
prose(format!(
"Status code {selected_code} selected, but this was not found."
))
.text_alignment(TextAlign::Center)
.text_color(palette::css::YELLOW),
)
}
} else {
OneOf3::C(
prose("No selection yet made. Select an item from the sidebar to continue.")
.text_alignment(TextAlign::Center),
)
};
fork(
flex_col((
FlexSpacer::Fixed(40.px()),
split(left_column, portal(sized_box(info_area).expand_width())).split_point(0.4),
))
.must_fill_major_axis(true),
worker(
|proxy, mut rx| async move {
while let Some(code) = rx.recv().await {
let proxy = proxy.clone();
tokio::task::spawn(async move {
let url = format!("https://http.cat/{code}");
let result = image_from_url(&url).await;
match result {
Ok(image) => drop(proxy.message((code, image))),
Err(err) => {
tracing::warn!(
"Loading image for HTTP status code {code} from {url} failed: {err:?}"
);
}
}
});
}
},
|state: &mut Self, sender| {
state.download_sender = Some(sender);
},
|state: &mut Self, (code, image): (u32, ImageData)| {
if let Some(status) = state.statuses.iter_mut().find(|it| it.code == code) {
status.image = ImageState::Available(image);
} else {
}
},
),
)
}
}
async fn image_from_url(url: &str) -> anyhow::Result<ImageData> {
let response = reqwest::get(url).await?;
let bytes = response.bytes().await?;
let image = image::load_from_memory(&bytes)?.into_rgba8();
let width = image.width();
let height = image.height();
let data = image.into_vec();
Ok(ImageData {
data: Blob::new(Arc::new(data)),
format: ImageFormat::Rgba8,
alpha_type: ImageAlphaType::Alpha,
width,
height,
})
}
impl Status {
fn list_view(&mut self) -> impl WidgetView<HttpCats> + use<> {
let code = self.code;
flex_row((
inline_prose(self.code.to_string()),
inline_prose(self.message),
FlexSpacer::Flex(1.),
text_button("Select", move |state: &mut HttpCats| {
let status = state
.statuses
.iter_mut()
.find(|it| it.code == code)
.unwrap();
if matches!(status.image, ImageState::NotRequested) {
state.download_sender.as_ref().unwrap().send(code).unwrap();
status.image = ImageState::Pending;
}
state.selected_code = Some(code);
}),
FlexSpacer::Fixed(Length::px(masonry::theme::SCROLLBAR_WIDTH)),
))
}
fn details_view(&mut self) -> impl WidgetView<HttpCats> + use<> {
let image = match &self.image {
ImageState::NotRequested => OneOf3::A(
prose("Failed to start fetching image. This is a bug!")
.text_alignment(TextAlign::Center),
),
ImageState::Pending => OneOf3::B(sized_box(spinner()).width(80.px()).height(80.px())),
ImageState::Available(image_data) => {
let attribution = sized_box(
sized_box(
prose("Copyright ©️ https://http.cat")
.line_break_mode(LineBreaking::Clip)
.text_alignment(TextAlign::End),
)
.padding(4.)
.corner_radius(4.)
.background_color(palette::css::BLACK.multiply_alpha(0.5)),
)
.padding(Padding {
left: 0.,
right: 42.,
top: 30.,
bottom: 0.,
});
OneOf3::C(zstack((
image(image_data.clone()),
attribution.alignment(UnitPoint::TOP_RIGHT),
)))
}
};
flex_col((
prose(format!("HTTP Status Code: {}", self.code)).text_alignment(TextAlign::Center),
prose(self.message)
.text_size(20.)
.text_alignment(TextAlign::Center),
FlexSpacer::Fixed(10.px()),
image,
))
.main_axis_alignment(xilem::view::MainAxisAlignment::Start)
}
}
fn run(event_loop: EventLoopBuilder) -> Result<(), EventLoopError> {
let data = HttpCats {
statuses: Status::parse_file(),
selected_code: None,
download_sender: None,
};
let app = Xilem::new_simple(
data,
HttpCats::view,
WindowOptions::new("HTTP cats").with_min_inner_size(LogicalSize::new(200., 200.)),
);
app.run_in(event_loop)
}
impl Status {
fn parse_file() -> Vec<Self> {
let mut lines = STATUS_CODES_CSV.lines();
let first_line = lines.next();
assert_eq!(first_line, Some("code,message"));
lines.flat_map(Self::parse_single).collect()
}
fn parse_single(line: &'static str) -> Option<Self> {
let (code, message) = line.split_once(',')?;
Some(Self {
code: code.parse().ok()?,
message: message.trim(),
image: ImageState::NotRequested,
})
}
}
const STATUS_CODES_CSV: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/resources/data/http_cats_status/status.csv",
));
#[expect(clippy::allow_attributes, reason = "No way to specify the condition")]
#[allow(dead_code, reason = "False positive: needed in not-_android version")]
fn main() -> Result<(), EventLoopError> {
run(EventLoop::with_user_event())
}
#[cfg(target_os = "android")]
#[expect(
unsafe_code,
reason = "We believe that there are no other declarations using this name in the compiled objects here"
)]
#[unsafe(no_mangle)]
fn android_main(app: winit::platform::android::activity::AndroidApp) {
use winit::platform::android::EventLoopBuilderExtAndroid;
let mut event_loop = EventLoop::with_user_event();
event_loop.with_android_app(app);
run(event_loop).expect("Can create app");
}