/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use async_trait::async_trait; use parking_lot::Mutex as SyncMutex; use rand::{rngs::OsRng, Rng}; use std::sync::Arc; use tracing::instrument; use crate::{ gfx::{gfxtag, DrawCall, DrawInstruction, Rectangle, RenderApi, Renderer}, mesh::{Color, MeshBuilder}, prop::{ BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyEnum, PropertyFloat32, PropertyRect, PropertyStr, PropertyUint32, Role, }, scene::{Pimpl, SceneNodeWeak}, text, util::i18n::I18nBabelFish, ExecutorPtr, }; use super::{DrawUpdate, OnModify, UIObject}; pub type TextPtr = Arc; pub struct Text { node: SceneNodeWeak, renderer: Renderer, i18n_fish: I18nBabelFish, tasks: SyncMutex>>, dc_key: u64, rect: PropertyRect, height: PropertyFloat32, z_index: PropertyUint32, priority: PropertyUint32, text: PropertyStr, font_size: PropertyFloat32, text_color: PropertyColor, lineheight: PropertyFloat32, text_align: PropertyEnum, overflow_wrap: PropertyEnum, use_i18n: PropertyBool, debug: PropertyBool, window_scale: PropertyFloat32, parent_rect: SyncMutex>, } impl Text { pub async fn new( node: SceneNodeWeak, window_scale: PropertyFloat32, renderer: Renderer, i18n_fish: I18nBabelFish, ) -> Pimpl { let node_ref = &node.upgrade().unwrap(); let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap(); let height = PropertyFloat32::wrap(node_ref, Role::Internal, "height", 0).unwrap(); let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap(); let priority = PropertyUint32::wrap(node_ref, Role::Internal, "priority", 0).unwrap(); let text = PropertyStr::wrap(node_ref, Role::Internal, "text", 0).unwrap(); let font_size = PropertyFloat32::wrap(node_ref, Role::Internal, "font_size", 0).unwrap(); let text_color = PropertyColor::wrap(node_ref, Role::Internal, "text_color").unwrap(); let lineheight = PropertyFloat32::wrap(node_ref, Role::Internal, "lineheight", 0).unwrap(); let text_align = PropertyEnum::wrap(node_ref, Role::Internal, "text_align", 0).unwrap(); let overflow_wrap = PropertyEnum::wrap(node_ref, Role::Internal, "overflow_wrap", 0).unwrap(); let use_i18n = PropertyBool::wrap(node_ref, Role::Internal, "use_i18n", 0).unwrap(); let debug = PropertyBool::wrap(node_ref, Role::Internal, "debug", 0).unwrap(); let self_ = Arc::new(Self { node, renderer, i18n_fish, tasks: SyncMutex::new(vec![]), dc_key: OsRng.gen(), rect, height, z_index, priority, text, font_size, text_color, lineheight, text_align, overflow_wrap, use_i18n, debug, window_scale, parent_rect: SyncMutex::new(None), }); Pimpl::Text(self_) } fn make_layout(&self) -> parley::Layout { let text = self.text.get(); let font_size = self.font_size.get(); let lineheight = self.lineheight.get(); let text_color = self.text_color.get(); let window_scale = self.window_scale.get(); let width = self.rect.get_width(); let text_align = self.text_align.get(); let overflow_wrap = self.overflow_wrap.get(); let text = if self.use_i18n.get() { if let Some(trans) = self.i18n_fish.tr(&text) { //t!("Translate '{text}' to '{trans}'"); trans } else { format!("tr err: {}", text) } } else { text }; text::make_layout2( &text, text_color, font_size, lineheight, window_scale, Some(width), &[], &[], &text_align, &overflow_wrap, ) } fn regen_mesh(&self, layout: &parley::Layout) -> Vec { let mut debug_opts = text::DebugRenderOptions::OFF; if self.debug.get() { debug_opts |= text::DebugRenderOptions::BASELINE; } text::render_layout_with_opts(layout, debug_opts, &self.renderer, gfxtag!("text")) } #[instrument(target = "ui::text")] async fn redraw(self: Arc, batch: BatchGuardPtr) { let Some(parent_rect) = self.parent_rect.lock().clone() else { warn!(target: "ui:text", "Skip draw since parent rect is empty"); return }; let atom = &mut batch.spawn(); let Some(draw_update) = self.get_draw_calls(atom, parent_rect) else { error!(target: "ui::text", "Text failed to draw"); return }; self.renderer.replace_draw_calls(Some(batch.id), draw_update.draw_calls); } fn get_draw_calls( &self, atom: &mut PropertyAtomicGuard, parent_rect: Rectangle, ) -> Option { self.rect.eval(atom, &parent_rect).ok()?; let rect = self.rect.get(); let layout = self.make_layout(); self.height.set(atom, layout.height()); let mut instrs = vec![DrawInstruction::Move(rect.pos())]; instrs.append(&mut self.regen_mesh(&layout)); if self.debug.get() { let rect = self.rect.get().with_zero_pos(); let mut mesh = MeshBuilder::new(gfxtag!("text_debug-rect")); mesh.draw_outline(&rect, [0., 1., 0., 0.7], 1.); let mesh = mesh.alloc(&self.renderer).draw_untextured(); instrs.push(DrawInstruction::Draw(mesh)); } Some(DrawUpdate { key: self.dc_key, draw_calls: vec![( self.dc_key, DrawCall::new(instrs, vec![], self.z_index.get(), "text"), )], }) } } #[async_trait] impl UIObject for Text { fn priority(&self) -> u32 { self.priority.get() } async fn start(self: Arc, ex: ExecutorPtr) { let me = Arc::downgrade(&self); let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone()); on_modify.when_change(self.rect.prop(), Self::redraw); on_modify.when_change(self.z_index.prop(), Self::redraw); on_modify.when_change(self.text.prop(), Self::redraw); on_modify.when_change(self.text_align.prop(), Self::redraw); on_modify.when_change(self.font_size.prop(), Self::redraw); on_modify.when_change(self.text_color.prop(), Self::redraw); on_modify.when_change(self.debug.prop(), Self::redraw); *self.tasks.lock() = on_modify.tasks; } fn stop(&self) { self.tasks.lock().clear(); *self.parent_rect.lock() = None; } #[instrument(target = "ui::text")] async fn draw( &self, parent_rect: Rectangle, atom: &mut PropertyAtomicGuard, ) -> Option { *self.parent_rect.lock() = Some(parent_rect); self.get_draw_calls(atom, parent_rect) } fn set_i18n(&self, i18n_fish: &I18nBabelFish) { self.i18n_fish.set(i18n_fish); } } impl Drop for Text { fn drop(&mut self) { let atom = self.renderer.make_guard(gfxtag!("Text::drop")); self.renderer .replace_draw_calls(Some(atom.batch_id), vec![(self.dc_key, Default::default())]); } } impl std::fmt::Debug for Text { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{:?}", self.node.upgrade().unwrap()) } }