diff --git a/Cargo.lock b/Cargo.lock index 12d70853e..849d0434b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1724,12 +1724,15 @@ dependencies = [ "freetype-rs", "futures", "gosub_interface", + "gosub_renderer", "gosub_shared", "gosub_svg", "image", "kurbo", "log", "once_cell", + "pango", + "pangocairo", "parley", "peniko", "skrifa 0.26.2", @@ -1908,6 +1911,7 @@ dependencies = [ "gosub_shared", "image", "log", + "pango", "url", "wasm-bindgen-futures", "web-sys", @@ -3455,6 +3459,32 @@ dependencies = [ "system-deps 7.0.3", ] +[[package]] +name = "pangocairo" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4690509a2fea2a6552a0ef8aa3e5f790c1365365ee0712afa1aedb39af3997b6" +dependencies = [ + "cairo-rs", + "glib", + "libc", + "pango", + "pangocairo-sys", +] + +[[package]] +name = "pangocairo-sys" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be6ac24147911a6a46783922fc288cf02f67570bc0d360e563b5b26aead6767" +dependencies = [ + "cairo-sys-rs", + "glib-sys", + "libc", + "pango-sys", + "system-deps 7.0.3", +] + [[package]] name = "parking_lot" version = "0.12.3" diff --git a/crates/gosub_cairo/Cargo.toml b/crates/gosub_cairo/Cargo.toml index 08953d681..95c71841a 100644 --- a/crates/gosub_cairo/Cargo.toml +++ b/crates/gosub_cairo/Cargo.toml @@ -10,6 +10,7 @@ license = "MIT" gosub_shared = { version = "0.1.1", registry = "gosub", path = "../gosub_shared" } gosub_interface = { version = "0.1.2", registry = "gosub", path = "../gosub_interface", features = [] } gosub_svg = { version = "0.1.1", registry = "gosub", path = "../gosub_svg", features = ["resvg"] } +gosub_renderer = { version = "0.1.1", registry = "gosub", path = "../gosub_renderer" } image = "0.25.2" smallvec = "1.13.2" anyhow = "1.0.89" @@ -24,3 +25,5 @@ uuid = { version = "1.11.0", features = ["v4"] } freetype-rs = "0.37.0" parley = "0.2.0" once_cell = "1.20.2" +pango = "0.20.7" +pangocairo = "0.20.7" \ No newline at end of file diff --git a/crates/gosub_cairo/src/elements.rs b/crates/gosub_cairo/src/elements.rs index ac0ed0f10..0ea79c4bd 100644 --- a/crates/gosub_cairo/src/elements.rs +++ b/crates/gosub_cairo/src/elements.rs @@ -4,5 +4,6 @@ pub(crate) mod color; pub(crate) mod gradient; pub(crate) mod image; pub(crate) mod rect; +// pub(crate) mod text; pub(crate) mod text; pub(crate) mod transform; diff --git a/crates/gosub_cairo/src/elements/brush.rs b/crates/gosub_cairo/src/elements/brush.rs index 8ae549aeb..6124f3a8c 100644 --- a/crates/gosub_cairo/src/elements/brush.rs +++ b/crates/gosub_cairo/src/elements/brush.rs @@ -25,6 +25,8 @@ impl GsBrush { } pub fn render(obj: &GsBrush, cr: &cairo::Context) { + // info!(target: "cairo", "GsBrush::render"); + match &obj { GsBrush::Solid(c) => { cr.set_source_rgba(c.r, c.g, c.b, c.a); diff --git a/crates/gosub_cairo/src/elements/rect.rs b/crates/gosub_cairo/src/elements/rect.rs index 10ca85977..bd3fd78b3 100644 --- a/crates/gosub_cairo/src/elements/rect.rs +++ b/crates/gosub_cairo/src/elements/rect.rs @@ -31,6 +31,8 @@ impl GsRect { } pub(crate) fn render(obj: &RenderRect, cr: &cairo::Context) { + // info!(target: "cairo", "GsRect::render"); + let x = obj.rect.x; let y = obj.rect.y; let width = obj.rect.width; diff --git a/crates/gosub_cairo/src/elements/text.rs b/crates/gosub_cairo/src/elements/text.rs index 001884c83..f768336d1 100644 --- a/crates/gosub_cairo/src/elements/text.rs +++ b/crates/gosub_cairo/src/elements/text.rs @@ -1,233 +1,40 @@ use crate::CairoBackend; -use gosub_interface::layout::{Decoration, TextLayout}; -use gosub_interface::render_backend::{RenderText, Text as TText}; -use gosub_shared::font::{Glyph, GlyphID}; -use gosub_shared::geo::FP; -use peniko::Font; -use skrifa::instance::NormalizedCoord; -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; +use gosub_interface::render_backend::RenderText; use crate::elements::brush::GsBrush; use crate::elements::color::GsColor; -use freetype::{Face, Library}; -use gosub_shared::ROBOTO_FONT; use kurbo::Stroke; use log::info; -use once_cell::sync::Lazy; -use parley::fontique::{FamilyId, SourceKind}; -use parley::{FontContext, GenericFamily}; - -/// Font manager that keeps track of fonts and faces -struct GosubFontContext { - /// Freetype library. Should be kept alive as long as any face is alive. - _library: Library, - /// Font context for parley to find fonts - font_ctx: FontContext, - /// Cache of any loaded font faces - face_cache: HashMap, - /// Default font face to use when a font cannot be found - default_face: Face, -} - -impl GosubFontContext { - /// Finds the face for the given family name, or returns the default face if no font is found. - fn find_face_family(&mut self, family: &str) -> &mut Face { - info!("Finding face for family: {}", family); - - // See if we already got the face in cache - if self.face_cache.contains_key(family) { - info!("Face found in cache"); - return self.face_cache.get_mut(family).expect("Face not found in cache"); - } - - // Parse the family name into a GenericFamily enum - let gf = GenericFamily::parse(family).unwrap_or(GenericFamily::SansSerif); - - // Find all the fonts for this family - let fids: Vec = self.font_ctx.collection.generic_families(gf).collect(); - if fids.is_empty() { - info!("No family found for family: {}", family); - return &mut self.default_face; - } - - // We only use the first font in the family - match self.font_ctx.collection.family(fids[0]) { - Some(f) => { - info!("Face found for family: {:?}", f.fonts()); - - // This first font can have multiple fonts (e.g. regular, bold, italic, etc.) - for font in f.fonts() { - match &font.source().kind { - SourceKind::Memory(blob) => { - info!("Loading font face from memory"); - let rc = Rc::new(blob.data().to_vec()); - - let face = self._library.new_memory_face(rc, 0).expect("Failed to load font face"); - self.face_cache.insert(family.to_string(), face); - } - SourceKind::Path(path) => { - info!("Loading font face from path {}", path.to_str().expect("path to string")); - - let face = self - ._library - .new_face(path.to_str().expect("path to string"), 0) - .expect("Failed to load font face"); - self.face_cache.insert(family.to_string(), face); - } - } - } - } - None => { - info!("No face found for family: {}", family); - } - } - - &mut self.default_face - } -} - -thread_local! { - /// We use a thread-local lazy static to ensure the font context is initialized once per thread - /// and is dropped when the thread exits. We need this because the FreeType library cannot be dropped - /// while any faces are still alive, so all is managed within this struct. - static LIB_FONT_FACE: Lazy> = Lazy::new(|| { - let lib = Library::init().expect("Failed to initialize FreeType"); - let rc = Rc::new(ROBOTO_FONT.to_vec()); - let default_face = lib.new_memory_face(rc, 0).expect("Failed to load font face"); - - // The FontContext struct holds the lib, ensuring it lives as long as all loaded faces - RefCell::new(GosubFontContext { - _library: lib, - font_ctx: FontContext::new(), - face_cache: HashMap::new(), - default_face, - }) - }); -} +use pango::Layout; +use gosub_renderer::font::Font as GsRenderFont; #[allow(unused)] #[derive(Clone, Debug)] pub struct GsText { - // List of glyphs we need to show - glyphs: Vec, - // Actual utf-8 text (we don't have this yet) + /// Actual utf-8 text text: String, - // Font we need to display (we need to have more info, like font familty, weight, etc.) - font: Font, - // Font size - fs: FP, - // List of coordinates for each glyph (?) - coords: Vec, - // Text decoration (strike-through, underline, etc.) - decoration: Decoration, -} - -impl TText for GsText { - type Font = Font; - - fn new(layout: &TL) -> Self - where - TL::Font: Into, - { - let font = layout.font().clone().into(); - let fs = layout.font_size(); - - let glyphs = layout - .glyphs() - .iter() - .map(|g| Glyph { - id: g.id as GlyphID, - x: g.x, - y: g.y, - }) - .collect(); - let coords = layout.coords().iter().map(|c| NormalizedCoord::from_bits(*c)).collect(); - - Self { - glyphs, - text: String::new(), - font, - fs, - coords, - decoration: layout.decorations().clone(), - } - } + /// Font in which we need to display the text + font: GsRenderFont, + /// Position of the text (top-left corner) + tl_pos: [f64; 2], } impl GsText { pub(crate) fn render(obj: &RenderText, cr: &cairo::Context) { - // let brush = &render.brush; - // let style: StyleRef = Fill::NonZero.into(); - // - // let transform = render.transform.map(|t| t).unwrap_or(Transform::IDENTITY); - // let brush_transform = render.brush_transform.map(|t| t); + info!(target: "cairo", "GsText::render"); - let base_x = obj.rect.x; - let base_y = obj.rect.y; - cr.move_to(base_x, base_y); + let pango_ctx = pangocairo::functions::create_context(cr); + let layout = Layout::new(&pango_ctx); + + let font_desc = &obj.font.get_font_description(); + layout.set_font_description(Some(&font_desc)); + layout.set_text(&obj.text); // Setup brush for rendering text GsBrush::render(&obj.brush, cr); - // This should be moved to the GosubFontContext::get_cairo_font_face(family: &str) method) - let font_face = unsafe { - LIB_FONT_FACE.with(|ctx_ref| { - let mut ctx = ctx_ref.borrow_mut(); - - let ft_face = ctx.find_face_family("sans-serif"); - let ft_face_ptr = ft_face.raw_mut() as *mut _ as *mut std::ffi::c_void; - let ff = cairo::ffi::cairo_ft_font_face_create_for_ft_face(ft_face_ptr, 0); - cairo::FontFace::from_raw_full(ff) - }) - }; - cr.set_font_face(&font_face); - - cr.set_font_size(obj.text.fs.into()); - - // Convert glyphs that are in parley / taffy format to cairo glyphs. Also make sure we - // offset the glyphs by the base_x and base_y. - let mut cairo_glyphs = vec![]; - for glyph in &obj.text.glyphs { - let cairo_glyph = cairo::Glyph::new(glyph.id as u64, base_x + glyph.x as f64, base_y + glyph.y as f64); - cairo_glyphs.push(cairo_glyph); - } - - _ = cr.show_glyphs(&cairo_glyphs); - - // Set decoration (underline, overline, line-through) - { - let decoration = &obj.text.decoration; - let _stroke = Stroke::new(decoration.width as f64); - - let c = decoration.color; - let brush = GsBrush::solid(GsColor::rgba32(c.0, c.1, c.2, 1.0)); - GsBrush::render(&brush, cr); - - let offset = decoration.x_offset as f64; - if decoration.underline { - let y = base_y + decoration.underline_offset as f64; - - cr.move_to(base_x + offset, y); - cr.line_to(base_x + obj.rect.width, y); - _ = cr.stroke(); - } - if decoration.overline { - let y = base_y - obj.rect.height; - - cr.move_to(base_x + offset, y); - cr.line_to(base_x + obj.rect.width, y); - _ = cr.stroke(); - } - - if decoration.line_through { - let y = base_y - obj.rect.height / 2.0; - - cr.move_to(base_x + offset, y); - cr.line_to(base_x + obj.rect.width, y); - _ = cr.stroke(); - } - } + cr.move_to(obj.rect.x.into(), obj.rect.y.into()); + cr.set_source_rgb(0.0, 0.0, 1.0); + pangocairo::functions::show_layout(cr, &layout); } } diff --git a/crates/gosub_cairo/src/font_manager.rs b/crates/gosub_cairo/src/font_manager.rs new file mode 100644 index 000000000..668c1c97c --- /dev/null +++ b/crates/gosub_cairo/src/font_manager.rs @@ -0,0 +1,35 @@ +use pango::prelude::{FontFamilyExt, FontMapExt}; + +/// This manager keeps track of all loaded fonts and provides a way to check if a font family is available. +pub struct FontManager { + font_map: pango::FontMap, +} + +impl FontManager { + pub fn new() -> Self { + let font_map = pangocairo::FontMap::new(); + Self { + font_map, + } + } + + pub fn has_font_family(&self, family: &str) -> bool { + self.font_map.list_families().iter().any(|f| f.name() == family) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_font_manager() { + let font_manager = FontManager::new(); + assert!(font_manager.has_font_family("Sans")); + assert!(font_manager.has_font_family("Serif")); + assert!(font_manager.has_font_family("Monospace")); + + assert!(!font_manager.has_font_family("Comic Sans")); + assert!(!font_manager.has_font_family("NOTAVAILBLE")); + } +} \ No newline at end of file diff --git a/crates/gosub_cairo/src/lib.rs b/crates/gosub_cairo/src/lib.rs index cda5baaa2..acdd42f90 100644 --- a/crates/gosub_cairo/src/lib.rs +++ b/crates/gosub_cairo/src/lib.rs @@ -5,6 +5,7 @@ use crate::elements::gradient::GsGradient; use crate::elements::image::GsImage; use crate::elements::rect::GsRect; use crate::elements::text::GsText; +use gosub_interface::font::Font as GsFont; use crate::elements::transform::GsTransform; use crate::render::window::{ActiveWindowData, WindowData}; use gosub_interface::render_backend::{RenderBackend, RenderRect, RenderText, Scene as _, WindowHandle}; @@ -20,6 +21,7 @@ mod elements; #[allow(unused)] pub mod render; mod scene; +mod font_manager; #[derive(Clone)] pub struct CairoBackend; @@ -36,7 +38,6 @@ impl RenderBackend for CairoBackend { type BorderSide = GsBorderSide; type BorderRadius = GsBorderRadius; type Transform = GsTransform; - type Text = GsText; type Gradient = GsGradient; type Color = GsColor; type Image = GsImage; @@ -129,4 +130,4 @@ impl Default for CairoBackend { fn default() -> Self { Self::new() } -} +} \ No newline at end of file diff --git a/crates/gosub_cairo/src/scene.rs b/crates/gosub_cairo/src/scene.rs index 6cc1c1dcd..239aa4670 100644 --- a/crates/gosub_cairo/src/scene.rs +++ b/crates/gosub_cairo/src/scene.rs @@ -7,6 +7,8 @@ use gosub_interface::render_backend::{ Point, Radius, RenderBackend, RenderRect, RenderText, Scene as TScene, Transform as TTransform, FP, }; use std::fmt::{Debug, Formatter}; +use pango::Layout; +use gosub_renderer::font::Font as GsRenderFont; /// A scene command that can be executed onto a cairo context. #[derive(Clone)] @@ -18,8 +20,8 @@ pub enum SceneCommand { // Draw a simple text without too much decoration and in a single font / color SimpleText { text: String, + font: GsRenderFont, pos: Point, - size: FP, }, // Group a list of commands together on a certain transform (translation, rotation, scale) Group { @@ -37,7 +39,14 @@ impl SceneCommand { } fn simple_text(text: String, pos: Point, size: FP) -> SceneCommand { - SceneCommand::SimpleText { text, pos, size } + let mut f = GsRenderFont::default(); + f.set_size(size as f32); + + SceneCommand::SimpleText { + text, + font: f, + pos, + } } } @@ -51,11 +60,11 @@ impl Debug for SceneCommand { .field("children", &children) .field("transform", &transform) .finish(), - SceneCommand::SimpleText { text, pos, size } => f + SceneCommand::SimpleText { text, font, pos } => f .debug_struct("SimpleText") .field("text", text) + .field("font", font) .field("pos", pos) - .field("size", size) .finish(), } } @@ -100,18 +109,17 @@ impl Scene { SceneCommand::Text(text) => { GsText::render(text, cr); } - SceneCommand::SimpleText { text, pos, size } => { - let face = - &cairo::FontFace::toy_create("sans-serif", cairo::FontSlant::Normal, cairo::FontWeight::Bold) - .unwrap(); - cr.set_font_face(face); - let fs: f32 = *size; - cr.set_font_size(fs as f64); + SceneCommand::SimpleText { text, font, pos} => { + let pango_ctx = pangocairo::functions::create_context(cr); + let layout = Layout::new(&pango_ctx); + + let font_desc = font.get_font_description(); + layout.set_font_description(Some(&font_desc)); + layout.set_text(text); cr.move_to(pos.x.into(), pos.y.into()); cr.set_source_rgb(0.0, 0.0, 1.0); - - _ = cr.show_text(text); + pangocairo::functions::show_layout(cr, &layout); } } } diff --git a/crates/gosub_interface/src/font.rs b/crates/gosub_interface/src/font.rs new file mode 100644 index 000000000..c5834b106 --- /dev/null +++ b/crates/gosub_interface/src/font.rs @@ -0,0 +1,54 @@ +pub trait Font { + fn new(family: &str, size: f32) -> Self; + fn family(&self) -> &str; + fn size(&self) -> f32; + fn weight(&self) -> FontWeight; + fn style(&self) -> FontStyle; + fn decoration(&self) -> FontDecoration; +} + +#[derive(Copy, Debug, Clone, PartialEq)] +pub enum FontWeight { + Thin, + Light, + Regular, + Medium, + Bold, + ExtraBold, +} + +#[derive(Copy, Debug, Clone, PartialEq)] +pub enum FontStyle { + Normal, + Italic, + Oblique, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct FontDecoration { + pub underline: bool, + pub strike_through: bool, +} + +impl Default for FontDecoration { + fn default() -> Self { + Self::new() + } +} + +impl FontDecoration { + pub fn new() -> Self { + FontDecoration { + underline: false, + strike_through: false, + } + } + + pub fn set_underline(&mut self, underline: bool) { + self.underline = underline; + } + + pub fn set_strike_through(&mut self, strike_through: bool) { + self.strike_through = strike_through; + } +} \ No newline at end of file diff --git a/crates/gosub_interface/src/layout.rs b/crates/gosub_interface/src/layout.rs index bdead5d23..61a68531a 100644 --- a/crates/gosub_interface/src/layout.rs +++ b/crates/gosub_interface/src/layout.rs @@ -1,9 +1,9 @@ use std::fmt::Debug; use crate::config::HasLayouter; -use gosub_shared::font::{Font, Glyph}; use gosub_shared::geo::{Point, Rect, Size, SizeU32}; use gosub_shared::types::Result; +use crate::font::Font; pub trait LayoutTree>: Sized + Debug + 'static { type NodeId: Debug + Copy + Clone + From + Into + PartialEq; @@ -140,15 +140,12 @@ pub trait HasTextLayout { pub trait TextLayout { type Font: Font; - fn dbg_layout(&self) -> String; - - fn size(&self) -> Size; - fn glyphs(&self) -> &[Glyph]; + fn text(&self) -> &str; - fn font(&self) -> &Self::Font; + fn dbg_layout(&self) -> String; - fn font_size(&self) -> f32; + fn font(&self) -> impl Font; fn coords(&self) -> &[i16]; diff --git a/crates/gosub_interface/src/lib.rs b/crates/gosub_interface/src/lib.rs index 4b5f7b39f..b5d73ca52 100644 --- a/crates/gosub_interface/src/lib.rs +++ b/crates/gosub_interface/src/lib.rs @@ -14,3 +14,4 @@ pub mod render_backend; pub mod render_tree; pub mod request; pub mod svg; +pub mod font; diff --git a/crates/gosub_interface/src/render_backend.rs b/crates/gosub_interface/src/render_backend.rs index 5dc63ebe4..394e5b672 100644 --- a/crates/gosub_interface/src/render_backend.rs +++ b/crates/gosub_interface/src/render_backend.rs @@ -1,4 +1,3 @@ -use crate::layout::TextLayout; use crate::svg::SvgRenderer; pub use gosub_shared::geo::*; use gosub_shared::types::Result; @@ -7,18 +6,19 @@ use smallvec::SmallVec; use std::fmt::{Debug, Display, Write}; use std::io; use std::ops::{Div, Mul, MulAssign}; +use crate::font::Font; pub trait WindowHandle: HasDisplayHandle + HasWindowHandle + Send + Sync + Clone {} impl WindowHandle for T where T: HasDisplayHandle + HasWindowHandle + Send + Sync + Clone {} pub trait RenderBackend: Sized + Debug { + type Font: Font + Clone + Debug; type Rect: Rect + Clone; type Border: Border + Clone + Debug; type BorderSide: BorderSide; type BorderRadius: BorderRadius; type Transform: Transform; - type Text: Text + Clone; type Gradient: Gradient; type Color: Color; type Image: Image; @@ -128,7 +128,8 @@ impl RenderRect { #[derive(Clone, Debug)] pub struct RenderText { - pub text: B::Text, + pub font: B::Font, + pub text: String, pub rect: B::Rect, pub transform: Option, pub brush: B::Brush, @@ -136,9 +137,10 @@ pub struct RenderText { } impl RenderText { - pub fn new(text: B::Text, rect: B::Rect, brush: B::Brush) -> Self { + pub fn new(text: &str, font: B::Font, rect: B::Rect, brush: B::Brush) -> Self { Self { - text, + text: text.to_string(), + font, rect, transform: None, brush, @@ -479,14 +481,6 @@ pub trait Transform: Sized + Mul + MulAssign + Clone + Send + Debug { } } -pub trait Text { - type Font; - - fn new(node: &TL) -> Self - where - TL::Font: Into; -} - pub struct ColorStop { pub offset: FP, pub color: B::Color, diff --git a/crates/gosub_renderer/Cargo.toml b/crates/gosub_renderer/Cargo.toml index 41620e29b..4ff7abe0e 100644 --- a/crates/gosub_renderer/Cargo.toml +++ b/crates/gosub_renderer/Cargo.toml @@ -15,6 +15,7 @@ anyhow = "1.0.94" image = "0.25.5" url = "2.5.4" log = "0.4.22" +pango = "0.20.7" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = "0.4.47" diff --git a/crates/gosub_renderer/src/draw.rs b/crates/gosub_renderer/src/draw.rs index 8ef684043..0fbf3c4a7 100644 --- a/crates/gosub_renderer/src/draw.rs +++ b/crates/gosub_renderer/src/draw.rs @@ -12,7 +12,7 @@ use gosub_interface::eventloop::EventLoopHandle; use gosub_interface::layout::{Layout, LayoutTree, Layouter, TextLayout}; use gosub_interface::render_backend::{ Border, BorderSide, BorderStyle, Brush, Color, ImageBuffer, ImgCache, NodeDesc, Rect, RenderBackend, RenderBorder, - RenderRect, RenderText, Scene as TScene, Text, Transform, + RenderRect, RenderText, Scene as TScene, Transform, }; use gosub_interface::render_tree; use gosub_interface::render_tree::RenderTreeNode as _; @@ -28,6 +28,7 @@ use std::future::Future; use std::sync::mpsc::Sender; use std::sync::{Arc, Mutex}; use url::Url; +use gosub_interface::font::Font; mod img; pub mod img_cache; @@ -77,12 +78,7 @@ impl TreeDrawerImpl { } } -impl, LayoutTree = RenderTree> + HasHtmlParser> TreeDrawer - for TreeDrawerImpl -where - <::Text as Text>::Font: - From<<::TextLayout as TextLayout>::Font>, -{ +impl, LayoutTree = RenderTree> + HasHtmlParser> TreeDrawer for TreeDrawerImpl { type ImgCache = ImageCache; fn draw(&mut self, size: SizeU32, el: &impl EventLoopHandle) -> ::Scene { @@ -366,11 +362,7 @@ struct Drawer<'s, 't, C: HasDrawComponents, EL: EventLoopHandle> { impl< C: HasDrawComponents, RenderTree = RenderTree> + HasHtmlParser, EL: EventLoopHandle, - > Drawer<'_, '_, C, EL> -where - <::Text as Text>::Font: - From<<::TextLayout as TextLayout>::Font>, -{ + > Drawer<'_, '_, C, EL> { pub(crate) fn render(&mut self, size: SizeU32) { let root = self.drawer.tree.root(); if let Err(e) = self.drawer.layouter.layout::(&mut self.drawer.tree, root, size) { @@ -402,7 +394,9 @@ where } fn render_node(&mut self, id: NodeId, pos: &mut Point) -> Result<()> { + // info!(target: "renderer", "Rendering node {id}"); let node = self.drawer.tree.get_node(id).ok_or(anyhow!("Node {id} not found"))?; + // dbg!(&node); let p = node.layout().rel_pos(); pos.x += p.x as FP; @@ -474,19 +468,7 @@ where fn render_text( node: &>::Node, pos: &Point, - scene: &mut ::Scene, -) where - <::Text as Text>::Font: - From<<::TextLayout as TextLayout>::Font>, -{ - // if u64::from(node.id) < 204 && u64::from(node.id) > 202 { - // return; - // } - - // if u64::from(node.id) == 203 { - // return; - // } - + scene: &mut ::Scene) { let color = node .props() .get("color") @@ -500,15 +482,14 @@ fn render_text( return; }; - let text: ::Text = - Text::new::<::TextLayout>(layout); - let size = node.layout().size(); - let rect = Rect::new(pos.x as FP, pos.y as FP, size.width as FP, size.height as FP); + let font = ::Font::new("Arial", 12.0); + let render_text = RenderText { - text, + text: layout.text().to_string(), + font, rect, transform: None, brush: Brush::color(color), diff --git a/crates/gosub_renderer/src/font.rs b/crates/gosub_renderer/src/font.rs new file mode 100644 index 000000000..57a871995 --- /dev/null +++ b/crates/gosub_renderer/src/font.rs @@ -0,0 +1,156 @@ +use pango::{FontDescription, Style, Weight}; +use gosub_interface::font::{Font as TFont, FontDecoration, FontStyle, FontWeight}; + +/// Implementation of the Font +#[derive(Debug, Clone, PartialEq)] +pub struct Font { + /// Font family used (ie: "Arial", "Times New Roman", etc.) + pub family: String, + /// Font size (defined by height in points) + pub size: f32, + /// Font weight (ie: "normal", "bold", etc.) + pub weight: FontWeight, + /// Font style (ie: "normal", "italic", etc.) + pub style: FontStyle, + /// Font decoration (ie: "none", "underline", "line-through", etc.) + pub decoration: FontDecoration, +} + +impl TFont for Font { + fn new(family: &str, size: f32) -> Self { + Font { + family: family.to_string(), + size, + weight: FontWeight::Regular, + style: FontStyle::Normal, + decoration: FontDecoration::default(), + } + } + + fn family(&self) -> &str { + self.family.as_str() + } + + fn size(&self) -> f32 { + self.size + } + + fn weight(&self) -> FontWeight { + self.weight + } + + fn style(&self) -> FontStyle { + self.style + } + + fn decoration(&self) -> FontDecoration { + self.decoration.clone() + } +} + +// Default implementation for Fonts +// https://granneman.com/webdev/coding/css/fonts-and-formatting/web-browser-font-defaults +// +// OS Browser Sans-serif Serif Mono +// Windows Firefox Arial Times New Roman Courier New +// Mac OS X Firefox Helvetica Times Courier +// Linux Firefox sans-serif serif monospace + +impl Default for Font { + fn default() -> Self { + Font { + #[cfg(target_os = "windows")] + family: "Arial".to_string(), + #[cfg(target_os = "macos")] + family: "Helvetica".to_string(), + #[cfg(target_os = "linux")] + family: "sans-serif".to_string(), + + size: 12.0, + weight: FontWeight::Regular, + style: FontStyle::Normal, + decoration: FontDecoration::default(), + } + } +} + +impl Font { + pub fn new(family: &str, size: f32) -> Self { + Font { + family: family.to_string(), + size, + weight: FontWeight::Regular, + style: FontStyle::Normal, + decoration: FontDecoration::new(), + } + } + + pub fn set_family(&mut self, family: &str) { + self.family = family.to_string(); + } + + pub fn set_size(&mut self, size: f32) { + self.size = size; + } + + pub fn set_weight(&mut self, weight: FontWeight) { + self.weight = weight; + } + + pub fn set_style(&mut self, style: FontStyle) { + self.style = style; + } + + pub fn set_decoration(&mut self, decoration: FontDecoration) { + self.decoration = decoration; + } + + pub fn get_font_description(&self) -> FontDescription { + let mut font_desc = FontDescription::new(); + + font_desc.set_family(&self.family); + + // Set the font size in Pango units (1 pt = 1024 Pango units) + font_desc.set_size((self.size * pango::SCALE as f32) as i32); + + // Map your FontWeight to Pango's Weight + let pango_weight = match self.weight { + FontWeight::Thin => Weight::Thin, + FontWeight::Light => Weight::Light, + FontWeight::Regular => Weight::Normal, + FontWeight::Medium => Weight::Medium, + FontWeight::Bold => Weight::Bold, + FontWeight::ExtraBold => Weight::Ultrabold, + }; + font_desc.set_weight(pango_weight); + + let pango_style = match self.style { + FontStyle::Normal => Style::Normal, + FontStyle::Italic => Style::Italic, + FontStyle::Oblique => Style::Oblique, + }; + font_desc.set_style(pango_style); + + font_desc + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_font_decoration() { + let mut decoration = FontDecoration::new(); + assert_eq!(decoration.underline, false); + assert_eq!(decoration.strike_through, false); + + decoration.set_underline(true); + assert_eq!(decoration.underline, true); + assert_eq!(decoration.strike_through, false); + + decoration.set_strike_through(true); + assert_eq!(decoration.underline, true); + assert_eq!(decoration.strike_through, true); + } +} diff --git a/crates/gosub_renderer/src/lib.rs b/crates/gosub_renderer/src/lib.rs index 9f53161f5..0b1747c88 100644 --- a/crates/gosub_renderer/src/lib.rs +++ b/crates/gosub_renderer/src/lib.rs @@ -1,3 +1,4 @@ mod debug; pub mod draw; pub mod render_tree; +pub mod font; diff --git a/crates/gosub_rendering/src/lib.rs b/crates/gosub_rendering/src/lib.rs index 949b917a5..89c2ab21c 100644 --- a/crates/gosub_rendering/src/lib.rs +++ b/crates/gosub_rendering/src/lib.rs @@ -6,4 +6,3 @@ pub mod position; // pub mod macos_render_tree; pub mod render_tree; -pub mod text; diff --git a/crates/gosub_rendering/src/render_tree.rs b/crates/gosub_rendering/src/render_tree.rs index eac3cae7a..1b74637b4 100644 --- a/crates/gosub_rendering/src/render_tree.rs +++ b/crates/gosub_rendering/src/render_tree.rs @@ -13,6 +13,7 @@ use gosub_shared::types::Result; use log::info; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; +use gosub_interface::font::Font; mod desc; @@ -801,7 +802,7 @@ impl LayoutNode for RenderTreeNode { fn text_size(&self) -> Option { if let RenderNodeData::Text(text) = &self.data { - text.layout.as_ref().map(|layout| layout.size()) + text.layout.as_ref().map(|layout| Size::new(layout.font().size(), layout.font().size())) } else { None } diff --git a/crates/gosub_rendering/src/text.rs b/crates/gosub_rendering/src/text.rs deleted file mode 100644 index 39fd36fb2..000000000 --- a/crates/gosub_rendering/src/text.rs +++ /dev/null @@ -1 +0,0 @@ -// fn measure_text() diff --git a/crates/gosub_taffy/src/compute/inline.rs b/crates/gosub_taffy/src/compute/inline.rs index f94df0533..cabfa3b84 100644 --- a/crates/gosub_taffy/src/compute/inline.rs +++ b/crates/gosub_taffy/src/compute/inline.rs @@ -17,7 +17,7 @@ use gosub_shared::font::Glyph; use gosub_shared::geo; use gosub_shared::ROBOTO_FONT; -use crate::text::{Font, TextLayout}; +use crate::text::TextLayout; use crate::{Display, LayoutDocument, TaffyLayouter}; static FONT_CX: LazyLock> = LazyLock::new(|| { diff --git a/crates/gosub_taffy/src/text.rs b/crates/gosub_taffy/src/text.rs index 0a4c526e5..81361f703 100644 --- a/crates/gosub_taffy/src/text.rs +++ b/crates/gosub_taffy/src/text.rs @@ -1,27 +1,11 @@ use gosub_interface::layout::{Decoration, TextLayout as TLayout}; -use gosub_shared::font::Font as TFont; -use gosub_shared::font::Glyph; use gosub_shared::geo::Size; -use parley::Font as PFont; - -#[derive(Debug, Clone)] -pub struct Font(pub PFont); - -impl TFont for Font { - fn to_bytes(&self) -> &[u8] { - self.0.data.data() - } -} - -impl From for PFont { - fn from(font: Font) -> Self { - font.0 - } -} +use gosub_interface::font::Font as GsFont; #[derive(Debug)] pub struct TextLayout { - pub glyphs: Vec, + pub text: String, + // pub glyphs: Vec, pub font: Font, pub font_size: f32, pub size: Size, @@ -30,28 +14,20 @@ pub struct TextLayout { } impl TLayout for TextLayout { - type Font = Font; + type Font = GsFont; - fn dbg_layout(&self) -> String { - format!("TextLayout: {:?}", self) - } - - fn size(&self) -> Size { - self.size + fn text(&self) -> &str { + self.text.as_str() } - fn glyphs(&self) -> &[Glyph] { - &self.glyphs + fn dbg_layout(&self) -> String { + format!("TextLayout: {:?}", self) } fn font(&self) -> &Self::Font { &self.font } - fn font_size(&self) -> f32 { - self.font_size - } - fn coords(&self) -> &[i16] { &self.coords } diff --git a/crates/gosub_vello/src/text.rs b/crates/gosub_vello/src/text.rs index a073c0c53..97338a9c8 100644 --- a/crates/gosub_vello/src/text.rs +++ b/crates/gosub_vello/src/text.rs @@ -1,6 +1,6 @@ use crate::VelloBackend; -use gosub_interface::layout::{Decoration, TextLayout}; -use gosub_interface::render_backend::{RenderText, Text as TText}; +use gosub_interface::layout::{TextLayout}; +use gosub_interface::render_backend::{RenderText}; use gosub_shared::geo::FP; use vello::kurbo::{Affine, Line, Stroke}; use vello::peniko::{Brush, Color, Fill, Font, StyleRef}; @@ -10,42 +10,9 @@ use vello_encoding::Glyph; #[derive(Clone)] pub struct Text { - glyphs: Vec, + text: String, font: Font, - fs: FP, coords: Vec, - decoration: Decoration, -} - -impl TText for Text { - type Font = Font; - fn new(layout: &TL) -> Self - where - TL::Font: Into, - { - let font = layout.font().clone().into(); - let fs = layout.font_size(); - - let glyphs = layout - .glyphs() - .iter() - .map(|g| Glyph { - id: g.id as u32, - x: g.x, - y: g.y, - }) - .collect(); - - let coords = layout.coords().iter().map(|c| NormalizedCoord::from_bits(*c)).collect(); - - Self { - glyphs, - font, - fs, - coords, - decoration: layout.decorations().clone(), - } - } } impl Text { diff --git a/examples/gtk-renderer/main.rs b/examples/gtk-renderer/main.rs index 3fd34f962..2ca8bb3f4 100644 --- a/examples/gtk-renderer/main.rs +++ b/examples/gtk-renderer/main.rs @@ -1,7 +1,7 @@ use futures::channel::mpsc; use futures::channel::mpsc::UnboundedSender; use futures::executor::block_on; -use futures::{SinkExt, StreamExt}; +use futures::StreamExt; use gosub_cairo::{CairoBackend, Scene}; use gosub_css3::system::Css3System; use gosub_html5::document::builder::DocumentBuilderImpl; @@ -94,7 +94,7 @@ fn build_ui(app: &Application, cl: &ApplicationCommandLine) -> i32 { let url = args .get(1) .and_then(|url| url.to_str()) - .unwrap_or("https://example.com") + .unwrap_or("https://gosub.io/tests/gopher.html") .to_string(); // Create a window and set the title @@ -120,7 +120,7 @@ fn build_ui(app: &Application, cl: &ApplicationCommandLine) -> i32 { // tree drawer. let area = DrawingArea::default(); - area.set_draw_func(move |area, cr, width, height| { + area.set_draw_func(move |_area, cr, width, height| { let size = SizeU32::new(width as u32, height as u32); let tx = instance.tx.clone(); @@ -146,9 +146,15 @@ fn build_ui(app: &Application, cl: &ApplicationCommandLine) -> i32 { }); }); + let scroll = gtk4::ScrolledWindow::builder() + .hscrollbar_policy(gtk4::PolicyType::Automatic) + .vscrollbar_policy(gtk4::PolicyType::Automatic) + .child(&area) + .build(); + window.set_child(Some(&scroll)); + window.set_default_width(800); window.set_default_height(600); - window.set_child(Some(&area)); window.present(); 1 diff --git a/examples/vello-renderer/event_loop.rs b/examples/vello-renderer/event_loop.rs index 2e9c5090a..d3aafd943 100644 --- a/examples/vello-renderer/event_loop.rs +++ b/examples/vello-renderer/event_loop.rs @@ -1,7 +1,6 @@ use crate::window::{Window, WindowState}; use gosub_instance::{DebugEvent, InstanceMessage}; use gosub_interface::config::ModuleConfiguration; -use gosub_interface::draw::TreeDrawer; use gosub_interface::input::InputEvent; use gosub_interface::render_backend::{Point, RenderBackend, SizeU32, FP}; use gosub_shared::types::Result; diff --git a/examples/vello-renderer/main.rs b/examples/vello-renderer/main.rs index bf45d6e3e..e34c40d6e 100644 --- a/examples/vello-renderer/main.rs +++ b/examples/vello-renderer/main.rs @@ -10,7 +10,7 @@ use gosub_html5::document::document_impl::DocumentImpl; use gosub_html5::document::fragment::DocumentFragmentImpl; use gosub_html5::parser::Html5Parser; use gosub_interface::config::{ - HasChrome, HasCssSystem, HasDocument, HasDrawComponents, HasHtmlParser, HasLayouter, HasRenderBackend, + HasChrome, HasCssSystem, HasDocument, HasHtmlParser, HasLayouter, HasRenderBackend, HasRenderTree, HasTreeDrawer, ModuleConfiguration, }; use gosub_renderer::draw::TreeDrawerImpl; diff --git a/examples/vello-renderer/window.rs b/examples/vello-renderer/window.rs index f88ee9473..b26c4e3af 100644 --- a/examples/vello-renderer/window.rs +++ b/examples/vello-renderer/window.rs @@ -1,27 +1,21 @@ use std::cell::LazyCell; use std::ops::Deref; -use std::sync::mpsc::Sender; use std::sync::Arc; -use crate::application::{CustomEventInternal, WindowOptions}; +use crate::application::WindowOptions; use crate::tabs::Tabs; use crate::WinitEventLoopHandle; use anyhow::anyhow; -use gosub_instance::{EngineInstance, InstanceHandle}; -use gosub_interface::chrome::ChromeHandle; use gosub_interface::config::ModuleConfiguration; -use gosub_interface::eventloop::EventLoopHandle; use gosub_interface::instance::{Handles, InstanceId}; -use gosub_interface::layout::LayoutTree; -use gosub_interface::render_backend::{ImageBuffer, NodeDesc, RenderBackend}; +use gosub_interface::render_backend::RenderBackend; use gosub_shared::geo::SizeU32; use gosub_shared::types::Result; use image::imageops::FilterType; -use log::{error, warn}; -use url::Url; +use log::warn; use winit::dpi::LogicalSize; use winit::event::Modifiers; -use winit::event_loop::{ActiveEventLoop, EventLoopProxy}; +use winit::event_loop::ActiveEventLoop; use winit::window::{Icon, Window as WinitWindow, WindowId}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -57,6 +51,7 @@ pub struct Window<'a, C: ModuleConfiguration> { pub(crate) renderer_data: ::WindowData<'a>, pub(crate) tabs: Tabs, pub(crate) mods: Modifiers, + #[allow(dead_code)] pub(crate) handles: Handles, }