Skip to main content

a2ui_tui/components/
video.rs

1//! Video component — renders a placeholder for videos in TUI.
2
3use ratatui::{
4    Frame,
5    layout::Rect,
6    style::{Color, Style},
7    text::{Line, Span},
8    widgets::Paragraph,
9};
10
11use a2ui_base::model::component_context::ComponentContext;
12use a2ui_base::protocol::common_types::DynamicString;
13use crate::component_impl::TuiComponent;
14
15/// Video component implementation.
16///
17/// TUI cannot play video. This component shows a placeholder
18/// with the video URL: `[▶ video_url]`.
19/// Applies a default 1-cell margin.
20pub struct VideoComponent;
21
22impl TuiComponent for VideoComponent {
23    fn name(&self) -> &'static str {
24        "Video"
25    }
26
27    fn render(
28        &self,
29        ctx: &ComponentContext,
30        area: Rect,
31        frame: &mut Frame,
32        _render_child: &mut dyn FnMut(&str, Rect, &mut Frame, &str),
33        _measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
34    ) {
35        let comp_model = match ctx.components.get(&ctx.component_id) {
36            Some(m) => m,
37            None => return,
38        };
39
40        // Apply default 1-cell margin on all sides.
41        let inner = Rect {
42            x: area.x + 1,
43            y: area.y + 1,
44            width: area.width.saturating_sub(2),
45            height: area.height.saturating_sub(2),
46        };
47
48        if inner.width == 0 || inner.height == 0 {
49            return;
50        }
51
52        // Resolve URL.
53        let url = match comp_model.get_property::<DynamicString>("url") {
54            Some(ds) => ctx.data_context.resolve_dynamic_string(&ds),
55            None => String::new(),
56        };
57
58        // Resolve posterUrl.
59        let poster = comp_model.get_property::<DynamicString>("posterUrl")
60            .map(|ds| ctx.data_context.resolve_dynamic_string(&ds));
61
62        let display_text = if !url.is_empty() { url } else { "video".to_string() };
63        let placeholder = if let Some(ref poster_url) = poster {
64            if !poster_url.is_empty() {
65                format!("[\u{25B6} {} | poster: {}]", display_text, poster_url)
66            } else {
67                format!("[\u{25B6} {}]", display_text)
68            }
69        } else {
70            format!("[\u{25B6} {}]", display_text)
71        };
72
73        let paragraph = Paragraph::new(Line::from(Span::styled(
74            placeholder,
75            Style::default().fg(Color::DarkGray),
76        )));
77        frame.render_widget(paragraph, inner);
78    }
79
80    fn natural_height(
81        &self,
82        _ctx: &ComponentContext,
83        _available_width: u16,
84        _measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
85    ) -> Option<u16> {
86        // Video scales to fit its area; no intrinsic height — authors grow it
87        // with `weight`.
88        None
89    }
90}