use crate::compat::Vec;
use crate::core::{Color, HorizontalAlignment, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::GenericSignal;
use crate::widget::metrics::role_font;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SubtitlePosition {
#[default]
Bottom,
Top,
}
pub struct SubtitleBar {
base: BaseWidget,
text: String,
max_lines: usize,
font_scale: f32,
background_opacity: u8,
position: SubtitlePosition,
pub text_changed: GenericSignal,
}
impl SubtitleBar {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Label, geometry, "SubtitleBar"),
text: String::new(),
max_lines: 3,
font_scale: 1.0,
background_opacity: 150,
position: SubtitlePosition::Bottom,
text_changed: GenericSignal::new(),
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn set_text(&mut self, text: impl Into<String>) {
let text = text.into();
if self.text == text {
return;
}
self.text = text;
self.base.request_redraw();
self.text_changed.emit();
}
pub fn clear(&mut self) {
self.set_text(String::new());
}
pub fn max_lines(&self) -> usize {
self.max_lines
}
pub fn set_max_lines(&mut self, max_lines: usize) {
let max_lines = max_lines.max(1);
if self.max_lines == max_lines {
return;
}
self.max_lines = max_lines;
self.base.request_redraw();
}
pub fn font_scale(&self) -> f32 {
self.font_scale
}
pub fn set_font_scale(&mut self, scale: f32) {
if !scale.is_finite() {
return;
}
let scale = scale.clamp(0.5, 3.0);
if (self.font_scale - scale).abs() < f32::EPSILON {
return;
}
self.font_scale = scale;
self.base.request_redraw();
}
pub fn background_opacity(&self) -> u8 {
self.background_opacity
}
pub fn set_background_opacity(&mut self, opacity: u8) {
if self.background_opacity == opacity {
return;
}
self.background_opacity = opacity;
self.base.request_redraw();
}
pub fn position(&self) -> SubtitlePosition {
self.position
}
pub fn set_position(&mut self, position: SubtitlePosition) {
if self.position == position {
return;
}
self.position = position;
self.base.request_redraw();
}
}
impl Widget for SubtitleBar {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn size_hint(&self) -> Size {
Size::new(320, 48)
}
impl_draw_bridge!();
}
impl EventHandler for SubtitleBar {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
impl Draw for SubtitleBar {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
if rect.width == 0 || rect.height == 0 || self.text.trim().is_empty() {
return;
}
let style = self.base.style();
let font = &role_font(style, 14.0 * self.font_scale);
let line_height = context.measure_text("M", font).height.max(1) + 2;
let ranges = crate::widget::text_utils::wrap_to_width(
&self.text,
rect.width.saturating_sub(16),
&mut |text: &str| context.measure_text(text, font).width,
);
let lines: Vec<_> = ranges.into_iter().take(self.max_lines).collect();
let height = (line_height * lines.len() as u32 + 8).min(rect.height.max(line_height));
let band = match self.position {
SubtitlePosition::Bottom => {
Rect::new(rect.x, rect.bottom() - height as i32, rect.width, height)
}
SubtitlePosition::Top => Rect::new(rect.x, rect.y, rect.width, height),
};
context.fill_rect(band, Color::rgba(0, 0, 0, self.background_opacity));
for (index, range) in lines.into_iter().enumerate() {
let Some(text) = self.text.get(range) else {
continue;
};
context.draw_text_fitted_cow(
Rect::new(
band.x + 8,
band.y + 4 + index as i32 * line_height as i32,
band.width.saturating_sub(16),
line_height,
),
text,
font,
Color::WHITE,
HorizontalAlignment::Center,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_caption_round_trips_and_the_line_cap_has_a_floor() {
let mut bar = SubtitleBar::new(Rect::new(0, 0, 320, 48));
bar.set_text("first line\nsecond line");
assert_eq!(bar.text(), "first line\nsecond line");
bar.set_max_lines(0);
assert_eq!(bar.max_lines(), 1);
bar.clear();
assert_eq!(bar.text(), "");
}
}