Skip to content

Commit

Permalink
feat: rich text
Browse files Browse the repository at this point in the history
  • Loading branch information
ten3roberts committed Jan 15, 2024
1 parent fff0a30 commit c8d770a
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 138 deletions.
109 changes: 36 additions & 73 deletions examples/basic.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
use flax::components::name;
use futures::StreamExt;
use futures_signals::signal::Mutable;
use futures::TryStreamExt;
use glam::{vec2, Vec2};
use image::{DynamicImage, ImageError};
use itertools::Itertools;
use palette::{named::WHITE, Hsla, Hsva, IntoColor, Srgba, WithAlpha};
use std::{path::PathBuf, time::Duration};
use palette::{Hsva, IntoColor, Srgba};
use std::path::PathBuf;
use tracing_subscriber::{
prelude::__tracing_subscriber_SubscriberExt, registry, util::SubscriberInitExt, EnvFilter,
};
use tracing_tree::HierarchicalLayer;
use violet::{
assets::AssetKey,
components::{
self, color, draw_shape, font_family, font_size, layout, size, text, Edges, FontFamily,
},
components::{self, layout, size, Edges},
layout::{CrossAlign, Direction, Layout},
shape,
style::StyleExt,
time::interval,
text::Style,
text::{FontFamily, TextSegment},
unit::Unit,
wgpu::font::FontFromFile,
widget::{Button, ContainerExt, Image, List, Rectangle, Stack, Text, WidgetExt},
App, Scope, StreamEffect, Widget,
App, Scope, Widget,
};

struct MainApp;
Expand Down Expand Up @@ -142,61 +138,6 @@ impl AssetKey for ImageFromPath {
}
}

struct Ticker;

impl Widget for Ticker {
fn mount(self, scope: &mut Scope) {
scope.spawn(StreamEffect::new(
interval(Duration::from_millis(50)).enumerate(),
move |scope: &mut Scope, (i, _)| {
scope.set(text(), format!("Tick: {:#?}", i % 64));
},
));

let font = FontFromFile {
path: PathBuf::from("assets/fonts/Inter/static/Inter-Regular.ttf"),
};

scope
.set(name(), "Counter".into())
.set(font_size(), 16.0)
.set(font_family(), "Inter/static/Inter-Regular.ttf".into())
.set(text(), "".into());
}
}

struct Counter {}

impl Widget for Counter {
fn mount(self, scope: &mut Scope<'_>) {
let count = Mutable::new(0);

List::new((
Sized::new(
Rectangle::new(Hsla::new(0.0, 0.5, 0.5, 1.0).into_color())
.with_margin(Edges::even(50.0)),
)
.with_size(Unit::px(vec2(100.0, 100.0))),
// SignalWidget::new(
// count
// .signal()
// .map(|count| Text::new(format!("Count: {}", count))),
// )
// .with_margin(MARGIN),
Sized::new(Button::new(
Hsla::new(200.0, 0.5, 0.5, 1.0).into_color(),
Hsla::new(200.0, 0.5, 0.2, 1.0).into_color(),
Box::new(move |_, _| {
*count.lock_mut() += 1;
}),
))
.with_min_size(Unit::px(vec2(100.0, 50.0)))
.with_size(Unit::px(vec2(100.0, 50.0))),
))
.mount(scope);
}
}

impl Widget for MainApp {
fn mount(self, scope: &mut Scope) {
scope
Expand Down Expand Up @@ -238,12 +179,35 @@ impl Widget for MainApp {
.collect_vec(),
)
.with_name("Images"),
Stack::new((
Text::new("The quick brown fox 🦊 jumps over the lazy dog 🐕")
.with_font("Inter/static/Inter-Bold.ttf")
.with_font_size(32.0)
.with_margin(MARGIN * 5.0),
))
Stack::new(
Text::rich([
TextSegment::new("Violet now has support for "),
TextSegment::new("rich ").with_style(Style::Italic),
TextSegment::new("text. I wanted to "),
TextSegment::new("emphasize").with_style(Style::Italic),
TextSegment::new(" that, "),
TextSegment::new("(and put something in bold)")
.with_family("Inter")
.with_style(Style::Normal),
TextSegment::new(", and").with_style(Style::Italic),
TextSegment::new(" also show off the different font loadings: \n"),
TextSegment::new(
"Monospace:\n\nfn main() { \n println!(\"Hello, world!\"); \n}",
)
.with_family(FontFamily::named("JetBrainsMono Nerd Font")),
])
.with_font_size(28.0)
.with_margin(MARGIN),
)
.with_background(Rectangle::new(EERIE_BLACK))
.with_padding(MARGIN),
Stack::new((Text::rich([TextSegment::new(
"The quick brown fox 🦊 jumps over the lazy dog 🐕",
)
.with_style(cosmic_text::Style::Italic)])
// .with_family("Inter")
.with_font_size(32.0)
.with_margin(MARGIN * 5.0),))
.with_background(Rectangle::new(EERIE_BLACK))
.with_padding(MARGIN)
.with_margin(MARGIN),
Expand All @@ -256,7 +220,6 @@ impl Widget for MainApp {
.with_size(Unit::px(vec2(100.0, 10.0)))
.with_margin(MARGIN),
Text::new("This is some text")
.with_font("Inter/static/Inter-Bold.ttf")
.with_font_size(16.0)
.with_margin(MARGIN),
Rectangle::new(EERIE_BLACK).with_size(Unit::rel(vec2(1.0, 1.0))),
Expand Down
37 changes: 2 additions & 35 deletions src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use palette::Srgba;
use crate::{
assets::Asset,
layout::{Layout, SizeResolver},
text::TextSegment,
unit::Unit,
};

Expand Down Expand Up @@ -58,7 +59,7 @@ component! {
/// A margin is in essence a minimum allowed distance to another items bounds
pub margin: Edges => [ Debuggable ],

pub text: String => [ ],
pub text: Vec<TextSegment> => [ ],
pub font_size: f32 => [ Debuggable ],

/// To retain consistent text wrapping between size query and the snug fitted rect the bounds
Expand All @@ -74,8 +75,6 @@ component! {

pub draw_shape(variant): () => [ Debuggable, Exclusive ],

pub font_family: FontFamily => [ Debuggable ],

pub(crate) size_resolver: Box<dyn SizeResolver>,
}

Expand Down Expand Up @@ -324,35 +323,3 @@ impl Rect {
Rect { min, max }
}
}

#[derive(Debug, Clone)]
pub struct FontFamily(pub Cow<'static, str>);

impl Display for FontFamily {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, f)
}
}

impl std::ops::Deref for FontFamily {
type Target = Cow<'static, str>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl FontFamily {
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self(name.into())
}
}

impl<T> From<T> for FontFamily
where
T: Into<Cow<'static, str>>,
{
fn from(value: T) -> Self {
Self::new(value)
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod shape;
mod stored;
pub mod style;
pub mod systems;
pub mod text;
pub mod time;
pub mod unit;
pub mod wgpu;
Expand Down
89 changes: 89 additions & 0 deletions src/text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
pub use cosmic_text::{Style, Weight};
use std::{borrow::Cow, fmt::Display};

#[derive(Debug, Clone)]
// Inspired by: https://github.com/pop-os/cosmic-text
pub enum FontFamily {
Named(Cow<'static, str>),

/// Serif fonts represent the formal text style for a script.
Serif,

/// Glyphs in sans-serif fonts, as the term is used in CSS, are generally low contrast
/// and have stroke endings that are plain — without any flaring, cross stroke,
/// or other ornamentation.
SansSerif,

/// Glyphs in cursive fonts generally use a more informal script style,
/// and the result looks more like handwritten pen or brush writing than printed letterwork.
Cursive,

/// Fantasy fonts are primarily decorative or expressive fonts that
/// contain decorative or expressive representations of characters.
Fantasy,

/// The sole criterion of a monospace font is that all glyphs have the same fixed width.
Monospace,
}

impl Display for FontFamily {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FontFamily::Named(name) => write!(f, "{name}"),
FontFamily::Serif => write!(f, "serif"),
FontFamily::SansSerif => write!(f, "sans-serif"),
FontFamily::Cursive => write!(f, "cursive"),
FontFamily::Fantasy => write!(f, "fantasy"),
FontFamily::Monospace => write!(f, "monospace"),
}
}
}

impl FontFamily {
pub fn named(name: impl Into<Cow<'static, str>>) -> Self {
Self::Named(name.into())
}
}

impl<T> From<T> for FontFamily
where
T: Into<Cow<'static, str>>,
{
fn from(value: T) -> Self {
Self::named(value)
}
}

/// A segment of rich text
pub struct TextSegment {
pub text: String,
pub family: FontFamily,
pub style: Style,
pub weight: Weight,
}

impl TextSegment {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
family: FontFamily::Serif,
style: Style::Normal,
weight: Weight::NORMAL,
}
}

pub fn with_family(mut self, family: impl Into<FontFamily>) -> Self {
self.family = family.into();
self
}

pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}

pub fn with_weight(mut self, weight: Weight) -> Self {
self.weight = weight;
self
}
}
47 changes: 36 additions & 11 deletions src/wgpu/components.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::sync::Arc;
use std::{borrow::Borrow, ffi::FromVecWithNulError, sync::Arc};

Check warning on line 1 in src/wgpu/components.rs

View workflow job for this annotation

GitHub Actions / check

unused import: `ffi::FromVecWithNulError`

Check warning on line 1 in src/wgpu/components.rs

View workflow job for this annotation

GitHub Actions / check

unused import: `ffi::FromVecWithNulError`

Check warning on line 1 in src/wgpu/components.rs

View workflow job for this annotation

GitHub Actions / Clippy

unused import: `ffi::FromVecWithNulError`

Check warning on line 1 in src/wgpu/components.rs

View workflow job for this annotation

GitHub Actions / Clippy

unused import: `ffi::FromVecWithNulError`

Check warning on line 1 in src/wgpu/components.rs

View workflow job for this annotation

GitHub Actions / test

unused import: `ffi::FromVecWithNulError`

Check warning on line 1 in src/wgpu/components.rs

View workflow job for this annotation

GitHub Actions / test

unused import: `ffi::FromVecWithNulError`

Check warning on line 1 in src/wgpu/components.rs

View workflow job for this annotation

GitHub Actions / test_miri

unused import: `ffi::FromVecWithNulError`

Check warning on line 1 in src/wgpu/components.rs

View workflow job for this annotation

GitHub Actions / test_miri

unused import: `ffi::FromVecWithNulError`

use cosmic_text::{Attrs, Buffer, FontSystem, Metrics, Shaping, Style};
use cosmic_text::{Attrs, Buffer, FontSystem, Metrics, Shaping};
use flax::component;
use fontdue::Font;

use crate::{
assets::Asset,
text::{FontFamily, TextSegment},
wgpu::{
graphics::texture::Texture,
shape_renderer::{DrawCommand, ObjectData},
Expand All @@ -18,8 +18,6 @@ component! {
/// The gpu texture to use for rendering
pub(crate) texture: Asset<Texture>,

pub(crate) font_handle: Asset<Font>,

/// Renderer specific data for drawing a shape
pub(crate) draw_cmd: DrawCommand,
pub(crate) object_data: ObjectData,
Expand All @@ -43,15 +41,29 @@ impl TextBufferState {
}
}

pub(crate) fn update(&mut self, font_system: &mut FontSystem, text: &str) {
self.buffer.set_text(
pub(crate) fn update(&mut self, font_system: &mut FontSystem, text: &[TextSegment]) {
self.buffer.set_rich_text(
font_system,
text,
Attrs::new()
.family(cosmic_text::Family::Name("Inter"))
.style(Style::Normal),
text.iter().map(|v| {
(
&*v.text,
Attrs::new()
.family((&v.family).into())
.style(v.style)
.weight(v.weight),
)
}),
Shaping::Advanced,
);
// self.buffer.set_text(
// font_system,
// text,
// Attrs::new()
// .family(cosmic_text::Family::Name("Inter"))
// .style(Style::Normal)
// .weight(400.0)
// Shaping::Advanced,
// );
}

pub(crate) fn buffer(&self) -> &Buffer {
Expand All @@ -62,3 +74,16 @@ impl TextBufferState {
&mut self.buffer
}
}

impl<'a> From<&'a FontFamily> for cosmic_text::Family<'a> {
fn from(value: &'a FontFamily) -> Self {
match value {
FontFamily::Named(name) => cosmic_text::Family::Name(name.borrow()),
FontFamily::Serif => cosmic_text::Family::Serif,
FontFamily::SansSerif => cosmic_text::Family::SansSerif,
FontFamily::Cursive => cosmic_text::Family::Cursive,
FontFamily::Fantasy => cosmic_text::Family::Fantasy,
FontFamily::Monospace => cosmic_text::Family::Monospace,
}
}
}
Loading

0 comments on commit c8d770a

Please sign in to comment.