use crate::{iotro::Language, Resolution}; use fontconfig::Fontconfig; use harfbuzz_rs::{Face, Font, Owned, UnicodeBuffer}; use std::sync::OnceLock; use svgwriter::{ tags::{Group, Path, Rect, TSpan, TagWithPresentationAttributes as _, Text}, Data, Graphic, Transform }; pub(crate) struct Question { res: Resolution, g: Group } impl Question { pub(crate) fn new(res: Resolution, lang: &Language<'_>, str: String) -> Self { static FONT: OnceLock>> = OnceLock::new(); let font = FONT.get_or_init(|| { let fc = Fontconfig::new().unwrap(); let font_path = fc.find("Noto Sans", None).unwrap().path; let face = Face::from_file(font_path, 0).unwrap(); Font::new(face) }); let upem = font.face().upem(); // constants let border_r = 12; let font_size = 44; let line_height = font_size * 6 / 5; let padding = font_size / 2; let margin_x = 240; let margin_y = padding * 3 / 2; let question_offset = 64; let question_width = 240; // calculated let box_width = 1920 - 2 * margin_x; let text_width = box_width - 2 * padding; // calculates the width of the given string let width_of = |s: &str| { let width: i32 = harfbuzz_rs::shape(font, UnicodeBuffer::new().add_str(s), &[]) .get_glyph_positions() .iter() .map(|glyph_pos| glyph_pos.x_advance) .sum(); (width * font_size) / upem as i32 }; let space_width = width_of(" "); // lay out the text let mut text = Text::new() .with_dominant_baseline("hanging") .with_transform( Transform::new().translate(padding, padding + font_size / 2 + border_r) ); let words = str.split_whitespace(); let mut text_height = 0; let mut text_x = 0; for word in words { let word_width = width_of(word); if text_x + word_width > text_width { text_x = 0; text_height += line_height; } text.push( TSpan::new() .with_x(text_x) .with_y(text_height) .append(word.to_owned()) ); text_x += word_width + space_width; } text_height += font_size; // calculated let box_height = text_height + 2 * padding + font_size / 2 + border_r; let mut g = Group::new() .with_fill("white") .with_font_family("Noto Sans") .with_font_size(font_size) .with_transform( Transform::new().translate(margin_x, 1080 - margin_y - box_height) ); let mut outline = Data::new(); outline.move_by(border_r, 0).horiz_line_to(question_offset); outline .vert_line_by(-font_size / 2) .arc_by(border_r, border_r, 0, false, true, border_r, -border_r) .horiz_line_by(question_width) .arc_by(border_r, border_r, 0, false, true, border_r, border_r) .vert_line_by(font_size) .arc_by(border_r, border_r, 0, false, true, -border_r, border_r) .horiz_line_by(-question_width) .arc_by(border_r, border_r, 0, false, true, -border_r, -border_r) .vert_line_by(-font_size / 2) .move_by(question_width + 2 * border_r, 0); outline .horiz_line_to(box_width - border_r) .arc_by(border_r, border_r, 0, false, true, border_r, border_r) .vert_line_by(box_height - 2 * border_r) .arc_by(border_r, border_r, 0, false, true, -border_r, border_r) .horiz_line_to(border_r) .arc_by(border_r, border_r, 0, false, true, -border_r, -border_r) .vert_line_to(border_r) .arc_by(border_r, border_r, 0, false, true, border_r, -border_r); g.push( Path::new() .with_stroke("#fff") .with_stroke_width(3) .with_fill("#000") .with_fill_opacity(".3") .with_d(outline) ); g.push( Text::new() .with_x(question_offset + question_width / 2 + border_r) .with_y(0) .with_dominant_baseline("middle") .with_text_anchor("middle") .with_font_weight(600) .append("Question") ); g.push(text); Self { res, g } } pub(crate) fn finish(self) -> Graphic { let mut svg = Graphic::new(); svg.set_width(self.res.width()); svg.set_height(self.res.height()); svg.set_view_box("0 0 1920 1080"); svg.push( Rect::new() .with_fill("#040") .with_x(0) .with_y(0) .with_width(1920) .with_height(1080) ); svg.push(self.g); svg } } #[cfg(test)] #[test] fn question() { let svg = Question::new( Resolution::FullHD, &Language::default(), "Hallo Welt! Dies ist eine sehr kluge Frage aus dem Publikum. Die Frage ist nämlich: Was ist eigentlich die Frage?".into() ) .finish(); std::fs::write("question.svg", svg.to_string_pretty()).unwrap(); }