use ratatui::{
Frame,
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::Paragraph,
};
use a2ui_base::model::component_context::ComponentContext;
use a2ui_base::protocol::common_types::DynamicString;
use crate::component_impl::TuiComponent;
fn render_placeholder(
variant_str: &str,
content: &str,
inner: Rect,
frame: &mut Frame,
) {
let placeholder = format!("[\u{1F5BC}{} {}]", variant_str, content);
let paragraph = Paragraph::new(Line::from(Span::styled(
placeholder,
Style::default().fg(Color::DarkGray),
)));
frame.render_widget(paragraph, inner);
}
pub struct ImageComponent;
impl TuiComponent for ImageComponent {
fn name(&self) -> &'static str {
"Image"
}
fn render(
&self,
ctx: &ComponentContext,
area: Rect,
frame: &mut Frame,
_render_child: &mut dyn FnMut(&str, Rect, &mut Frame, &str),
_measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
) {
let comp_model = match ctx.components.get(&ctx.component_id) {
Some(m) => m,
None => return,
};
let inner = crate::layout_engine::padded_content(area);
if inner.width == 0 || inner.height == 0 {
return;
}
let description = match comp_model.get_property::<DynamicString>("description") {
Some(ds) => ctx.data_context.resolve_dynamic_string(&ds),
None => String::new(),
};
let url = match comp_model.get_property::<DynamicString>("url") {
Some(ds) => ctx.data_context.resolve_dynamic_string(&ds),
None => String::new(),
};
let _fit: Option<String> = comp_model.get_property("fit");
let variant: Option<String> = comp_model.get_property("variant");
let variant_str = variant.as_deref().map(|v| format!(" ({})", v)).unwrap_or_default();
let content = if !description.is_empty() {
description
} else if !url.is_empty() {
url.clone()
} else {
"image".to_string()
};
if let Ok(()) = real::render(&url, inner, frame) {
return;
}
render_placeholder(&variant_str, &content, inner, frame);
}
fn natural_height(
&self,
_ctx: &ComponentContext,
_available_width: u16,
_measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
) -> Option<u16> {
Some(3)
}
}
mod real {
use std::sync::OnceLock;
use ratatui::{Frame, layout::{Rect, Size}};
use ratatui_image::{
Image, Resize,
picker::{Picker, ProtocolType},
protocol::Protocol,
};
fn picker() -> &'static Picker {
static PICKER: OnceLock<Picker> = OnceLock::new();
PICKER.get_or_init(|| match Picker::from_query_stdio() {
Ok(p) => p,
Err(_) => Picker::halfblocks(),
})
}
pub fn protocol_name() -> &'static str {
match picker().protocol_type() {
ProtocolType::Halfblocks => "Halfblocks",
ProtocolType::Sixel => "Sixel",
ProtocolType::Kitty => "Kitty",
ProtocolType::Iterm2 => "iTerm2",
}
}
pub fn render(path: &str, inner: Rect, frame: &mut Frame) -> Result<(), ()> {
if path.is_empty() || path.starts_with("http://") || path.starts_with("https://") {
return Err(());
}
let path = std::path::Path::new(path);
if !path.is_file() {
return Err(());
}
let dyn_image = image::ImageReader::open(path)
.map_err(|_| ())?
.with_guessed_format()
.map_err(|_| ())?
.decode()
.map_err(|_| ())?;
let picker = picker();
let size = Size {
width: inner.width,
height: inner.height,
};
let protocol: Protocol = picker
.new_protocol(dyn_image, size, Resize::Fit(None))
.map_err(|_| ())?;
frame.render_widget(Image::new(&protocol), inner);
Ok(())
}
}
pub fn detected_protocol() -> &'static str {
real::protocol_name()
}