diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ffdd9e030..81869a046 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,6 +74,4 @@ jobs: - name: test (kas-theme) run: cargo test --manifest-path kas-theme/Cargo.toml - name: test (kas-wgpu) - run: | - cargo test --manifest-path kas-wgpu/Cargo.toml - cargo test --manifest-path kas-wgpu/Cargo.toml --features clipboard,shaping + run: cargo test --manifest-path kas-wgpu/Cargo.toml --features clipboard diff --git a/Cargo.toml b/Cargo.toml index fd4561781..f6588999a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,8 @@ rustdoc-args = ["--cfg", "doc_cfg"] # RUSTDOCFLAGS="--cfg doc_cfg" cargo +nightly doc --features=nightly,internal_doc,markdown,yaml,json --all --no-deps --open [features] +default = ["shaping"] + # Enables usage of unstable Rust features nightly = ["min_spec"] @@ -34,10 +36,10 @@ min_spec = [] # This flag does not change the API, only built documentation. internal_doc = [] -# Enables text shaping via HarfBuzz -# Shaping is part of Complex Text Layout, used for ligatures and where form -# depends on position and context (especially important for Arabic). +# Enable shaping via rustybuzz shaping = ["kas-text/shaping"] +# Force use of HarfBuzz for shaping +harfbuzz = ["kas-text/harfbuzz"] # Enable Markdown parsing markdown = ["kas-text/markdown"] @@ -45,7 +47,7 @@ markdown = ["kas-text/markdown"] # Enable config read/write #TODO(cargo): once weak-dep-features (cargo#8832) is stable, add "winit?/serde" # and remove the serde feature requirement under dependencies.winit. -config = ["serde"] +config = ["serde", "kas-text/serde"] # Enable support for YAML (de)serialisation yaml = ["config", "serde_yaml"] @@ -87,4 +89,4 @@ features = ["serde"] members = ["kas-macros", "kas-theme", "kas-wgpu"] [patch.crates-io] -kas-text = { git = "https://github.com/kas-gui/kas-text.git", rev = "df3fcc1b6a8f8644c0b799021045248bc29098c8" } +kas-text = { git = "https://github.com/kas-gui/kas-text.git", rev = "c594569d5d8a4640c2b76865114453b2701d0c35" } diff --git a/README.md b/README.md index 6ccca630b..0510268ba 100644 --- a/README.md +++ b/README.md @@ -132,12 +132,6 @@ Currently, KAS's only drawing method is [WebGPU] which requires DirectX 11/12, Vulkan or Metal. In the future, there may be support for OpenGL and software rendering. -#### HarfBuzz (optional) - -This is only needed if the `shaping` feature is enabled. On my system, the -following libraries are used: `libharfbuzz.so.0`, `libglib-2.0.so.0`, -`libgraphite2.so.3` and `libpcre.so.1`. - ### Quick-start Install dependencies: @@ -184,8 +178,8 @@ and runs the UI. The `kas` crate has the following feature flags: -- `shaping`: enables complex glyph forming for languages such as Arabic. - This requires that the HarfBuzz library is installed. +- `shaping` (enabled by default): enables complex glyph forming for languages such as Arabic. + Alternate: `harfbuzz` forces use of the HarfBuzz library for shaping. - `markdown`: enables Markdown parsing for rich-text - `config`: adds (de)serialisation support for configuration plus a few utility types (specifying `serde` instead only implements for utility types) diff --git a/example-config/theme.yaml b/example-config/theme.yaml new file mode 100644 index 000000000..103b89510 --- /dev/null +++ b/example-config/theme.yaml @@ -0,0 +1,45 @@ +--- +font_size: 10.0 +active_scheme: dark +color_schemes: + "": + background: "#FFFFFF" + frame: "#DADADA" + bg: "#FFFFFF" + bg_disabled: "#EDEDED" + bg_error: "#FFBCBC" + text: "#000000" + text_sel: "#FFFFFF" + text_sel_bg: "#6CC0E1" + label_text: "#000000" + button_text: "#FFFFFF" + nav_focus: "#F3D3AA" + button: "#7CDAFF" + button_disabled: "#BCBCBC" + button_highlighted: "#89E7FF" + button_depressed: "#6CC0E1" + checkbox: "#7CDAFF" + dark: + background: "#4d4f50" + frame: "#AAAAAA" + bg: "#595959" + bg_disabled: "#959595" + bg_error: "#FFBCBC" + text: "#FFFFFF" + text_sel: "#FFFFFF" + text_sel_bg: "#CB9559" + label_text: "#FFFFFF" + button_text: "#FFFFFF" + nav_focus: "#FFDABC" + button: "#BC5959" + button_disabled: "#DADADA" + button_highlighted: "#CB9559" + button_depressed: "#955959" + checkbox: "#BC5959" +font_aliases: + sans-serif: + mode: Prepend + list: [Calibri] + Calibri: + mode: Append + list: [Carlito] diff --git a/kas-theme/src/config.rs b/kas-theme/src/config.rs index 4ac3b2e3a..36b3ba2c3 100644 --- a/kas-theme/src/config.rs +++ b/kas-theme/src/config.rs @@ -6,6 +6,7 @@ //! Theme configuration use crate::{ColorsLinear, ColorsSrgb, ThemeConfig}; +use kas::text::fonts::{fonts, AddMode}; use kas::TkAction; use std::collections::BTreeMap; @@ -16,16 +17,23 @@ pub struct Config { #[cfg_attr(feature = "config", serde(skip))] dirty: bool, + /// Standard font size, in units of points-per-Em #[cfg_attr(feature = "config", serde(default = "defaults::font_size"))] font_size: f32, + /// The colour scheme to use #[cfg_attr(feature = "config", serde(default))] active_scheme: String, + /// All colour schemes /// TODO: possibly we should not save default schemes and merge when /// loading (perhaps via a `PartialConfig` type). #[cfg_attr(feature = "config", serde(default = "defaults::color_schemes",))] color_schemes: BTreeMap, + + /// Font aliases, used when searching for a font family matching the key. + #[cfg_attr(feature = "config", serde(default))] + font_aliases: BTreeMap, } impl Default for Config { @@ -35,6 +43,7 @@ impl Default for Config { font_size: defaults::font_size(), active_scheme: Default::default(), color_schemes: defaults::color_schemes(), + font_aliases: Default::default(), } } } @@ -116,6 +125,28 @@ impl ThemeConfig for Config { fn is_dirty(&self) -> bool { self.dirty } + + /// Apply config effects which only happen on startup + fn apply_startup(&self) { + if !self.font_aliases.is_empty() { + fonts().update_db(|db| { + for (family, aliases) in self.font_aliases.iter() { + db.add_aliases( + family.to_string().into(), + aliases.list.iter().map(|s| s.to_string().into()), + aliases.mode, + ); + } + }); + } + } +} + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "config", derive(serde::Serialize, serde::Deserialize))] +pub struct FontAliases { + mode: AddMode, + list: Vec, } mod defaults { diff --git a/kas-theme/src/dim.rs b/kas-theme/src/dim.rs index 33c917bf7..8fda88fed 100644 --- a/kas-theme/src/dim.rs +++ b/kas-theme/src/dim.rs @@ -66,7 +66,11 @@ impl Dimensions { let font_id = Default::default(); let dpp = scale_factor * (96.0 / 72.0); let dpem = dpp * pt_size; - let line_height = i32::conv_ceil(kas::text::fonts::fonts().get(font_id).height(dpem)); + let line_height = i32::conv_ceil( + kas::text::fonts::fonts() + .get_first_face(font_id) + .height(dpem), + ); let outer_margin = (params.outer_margin * scale_factor).cast_nearest(); let inner_margin = (params.inner_margin * scale_factor).cast_nearest(); diff --git a/kas-theme/src/flat_theme.rs b/kas-theme/src/flat_theme.rs index 9515f170e..3d62f617e 100644 --- a/kas-theme/src/flat_theme.rs +++ b/kas-theme/src/flat_theme.rs @@ -110,7 +110,7 @@ where } fn init(&mut self, _draw: &mut D) { - if let Err(e) = kas::text::fonts::fonts().load_default() { + if let Err(e) = kas::text::fonts::fonts().select_default() { panic!("Error loading font: {}", e); } } diff --git a/kas-theme/src/shaded_theme.rs b/kas-theme/src/shaded_theme.rs index ed4003bed..fed5ee531 100644 --- a/kas-theme/src/shaded_theme.rs +++ b/kas-theme/src/shaded_theme.rs @@ -99,7 +99,7 @@ where } fn init(&mut self, _draw: &mut D) { - if let Err(e) = kas::text::fonts::fonts().load_default() { + if let Err(e) = kas::text::fonts::fonts().select_default() { panic!("Error loading font: {}", e); } } diff --git a/kas-theme/src/traits.rs b/kas-theme/src/traits.rs index 9682476bc..51d9075c4 100644 --- a/kas-theme/src/traits.rs +++ b/kas-theme/src/traits.rs @@ -12,7 +12,10 @@ use std::ops::{Deref, DerefMut}; /// Requirements on theme config (without `config` feature) #[cfg(not(feature = "config"))] -pub trait ThemeConfig: Clone + std::fmt::Debug + 'static {} +pub trait ThemeConfig: Clone + std::fmt::Debug + 'static { + /// Apply startup effects + fn apply_startup(&self); +} /// Requirements on theme config (with `config` feature) #[cfg(feature = "config")] @@ -21,6 +24,9 @@ pub trait ThemeConfig: { /// Has the config ever been updated? fn is_dirty(&self) -> bool; + + /// Apply startup effects + fn apply_startup(&self); } /// A *theme* provides widget sizing and drawing implementations. diff --git a/kas-wgpu/Cargo.toml b/kas-wgpu/Cargo.toml index 58c1c1a44..f4a3c35a3 100644 --- a/kas-wgpu/Cargo.toml +++ b/kas-wgpu/Cargo.toml @@ -27,9 +27,6 @@ gat = ["kas-theme/gat"] # Enables clipboard read/write clipboard = ["window_clipboard"] -# Enables text shaping -shaping = ["kas/shaping"] - # Use stack_dst crate for sized unsized types stack_dst = ["kas-theme/stack_dst"] diff --git a/kas-wgpu/src/draw/text_pipe.rs b/kas-wgpu/src/draw/text_pipe.rs index 9857df093..bfc2d872f 100644 --- a/kas-wgpu/src/draw/text_pipe.rs +++ b/kas-wgpu/src/draw/text_pipe.rs @@ -10,7 +10,7 @@ use ab_glyph::{Font, FontRef}; use kas::cast::*; use kas::draw::{color::Rgba, Pass}; use kas::geom::{Quad, Vec2}; -use kas::text::fonts::{fonts, FontId}; +use kas::text::fonts::{fonts, FaceId}; use kas::text::{Effect, Glyph, TextDisplay}; use std::collections::hash_map::{Entry, HashMap}; use std::mem::size_of; @@ -43,15 +43,15 @@ impl SpriteDescriptor { (30.0 / height).round().clamp(1.0, 15.0) } - fn new(font: FontId, glyph: Glyph, height: f32) -> Self { - let font: u16 = font.get().cast(); + fn new(face: FaceId, glyph: Glyph, height: f32) -> Self { + let face: u16 = face.get().cast(); let glyph_id: u16 = glyph.id.0; let mult = Self::sub_pixel_from_height(height); let height: u32 = (height * SCALE_MULT).cast_nearest(); let x_off: u8 = (glyph.position.0.fract() * mult).cast_nearest(); let y_off: u8 = (glyph.position.1.fract() * mult).cast_nearest(); assert!(height & 0xFF00_0000 == 0 && x_off & 0xF0 == 0 && y_off & 0xF0 == 0); - let packed = font as u64 + let packed = face as u64 | ((glyph_id as u64) << 16) | ((height as u64) << 32) | ((x_off as u64) << 56) @@ -59,7 +59,7 @@ impl SpriteDescriptor { SpriteDescriptor(packed) } - fn font(self) -> usize { + fn face(self) -> usize { (self.0 & 0x0000_0000_0000_FFFF) as usize } @@ -98,7 +98,7 @@ struct Sprite { impl atlases::Pipeline { fn rasterize( &mut self, - font: &FontRef<'static>, + face: &FontRef<'static>, desc: SpriteDescriptor, ) -> Option<(Sprite, (u32, u32), (u32, u32), Vec)> { let fract_pos = desc.fractional_position(); @@ -107,12 +107,15 @@ impl atlases::Pipeline { scale: desc.height().into(), position: fract_pos.into(), }; - let outline = font.outline_glyph(glyph)?; + let outline = face.outline_glyph(glyph)?; let bounds = outline.px_bounds(); let size = to_vec2(bounds.max - bounds.min); let offset = to_vec2(bounds.min) - Vec2(fract_pos.0.round(), fract_pos.1.round()); let size_u32 = (u32::conv_trunc(size.0), u32::conv_trunc(size.1)); + if size_u32.0 == 0 || size_u32.1 == 0 { + return None; // nothing to draw + } let (atlas, _, origin, tex_quad) = match self.allocate(size_u32) { Ok(result) => result, @@ -159,7 +162,7 @@ unsafe impl bytemuck::Pod for Instance {} /// A pipeline for rendering text pub struct Pipeline { atlas_pipe: atlases::Pipeline, - fonts: Vec>, + faces: Vec>, glyphs: HashMap>, prepare: Vec<(u32, (u32, u32), (u32, u32), Vec)>, } @@ -194,26 +197,26 @@ impl Pipeline { ); Pipeline { atlas_pipe, - fonts: Default::default(), + faces: Default::default(), glyphs: Default::default(), prepare: Default::default(), } } - /// Prepare fonts + /// Prepare font faces /// /// This must happen before any drawing is queued. TODO: perhaps instead /// use temporary IDs for unrastered glyphs and update in `prepare`? pub fn prepare_fonts(&mut self) { let fonts = fonts(); - let n1 = self.fonts.len(); - let n2 = fonts.num_fonts(); + let n1 = self.faces.len(); + let n2 = fonts.num_faces(); if n2 > n1 { - let font_data = fonts.font_data(); + let face_data = fonts.face_data(); for i in n1..n2 { - let (data, index) = font_data.get_data(i); - let font = FontRef::try_from_slice_and_index(data, index).unwrap(); - self.fonts.push(font); + let (data, index) = face_data.get_data(i); + let face = FontRef::try_from_slice_and_index(data, index).unwrap(); + self.faces.push(face); } } } @@ -276,8 +279,8 @@ impl Pipeline { Entry::Vacant(entry) => { // NOTE: we only need the allocation and coordinates now; the // rendering could be offloaded. - let font = &self.fonts[desc.font()]; - let result = self.atlas_pipe.rasterize(font, desc); + let face = &self.faces[desc.face()]; + let result = self.atlas_pipe.rasterize(face, desc); let sprite = if let Some((sprite, origin, size, data)) = result { self.prepare.push((sprite.atlas, origin, size, data)); Some(sprite) @@ -326,8 +329,8 @@ impl Window { ) { let time = std::time::Instant::now(); - let for_glyph = |font: FontId, _, height: f32, glyph: Glyph| { - let desc = SpriteDescriptor::new(font, glyph, height); + let for_glyph = |face: FaceId, _, height: f32, glyph: Glyph| { + let desc = SpriteDescriptor::new(face, glyph, height); if let Some(sprite) = pipe.get_glyph(desc) { let pos = pos + Vec2::from(glyph.position); let a = pos + sprite.offset; @@ -366,8 +369,8 @@ impl Window { let time = std::time::Instant::now(); let mut rects = vec![]; - let mut for_glyph = |font: FontId, _, height: f32, glyph: Glyph, _: usize, _: ()| { - let desc = SpriteDescriptor::new(font, glyph, height); + let mut for_glyph = |face: FaceId, _, height: f32, glyph: Glyph, _: usize, _: ()| { + let desc = SpriteDescriptor::new(face, glyph, height); if let Some(sprite) = pipe.get_glyph(desc) { let pos = pos + Vec2::from(glyph.position); let a = pos + sprite.offset; @@ -397,7 +400,7 @@ impl Window { }; text.glyphs_with_effects(effects, (), for_glyph, for_rect); } else { - text.glyphs(|font, dpu, height, glyph| for_glyph(font, dpu, height, glyph, 0, ())); + text.glyphs(|face, dpu, height, glyph| for_glyph(face, dpu, height, glyph, 0, ())); } self.duration += time.elapsed(); @@ -427,8 +430,8 @@ impl Window { let time = std::time::Instant::now(); let mut rects = vec![]; - let for_glyph = |font: FontId, _, height: f32, glyph: Glyph, _, col: Rgba| { - let desc = SpriteDescriptor::new(font, glyph, height); + let for_glyph = |face: FaceId, _, height: f32, glyph: Glyph, _, col: Rgba| { + let desc = SpriteDescriptor::new(face, glyph, height); if let Some(sprite) = pipe.get_glyph(desc) { let pos = pos + Vec2::from(glyph.position); let a = pos + sprite.offset; diff --git a/kas-wgpu/src/options.rs b/kas-wgpu/src/options.rs index 91144e5e9..cf341a5e6 100644 --- a/kas-wgpu/src/options.rs +++ b/kas-wgpu/src/options.rs @@ -196,6 +196,7 @@ impl Options { } } } + theme.config().apply_startup(); Ok(()) } diff --git a/src/text.rs b/src/text.rs index 9653b6634..44ed00bef 100644 --- a/src/text.rs +++ b/src/text.rs @@ -67,7 +67,7 @@ pub mod util { /// Note: an alternative approach would be to delay text preparation by /// adding TkAction::PREPARE and a new method, perhaps in Layout. fn prepare_if_needed(text: &mut Text, avail: Size) -> TkAction { - if fonts::fonts().num_fonts() == 0 { + if fonts::fonts().num_faces() == 0 { // Fonts not loaded yet: cannot prepare and can assume it will happen later anyway. return TkAction::empty(); }