Skip to content

Commit 070abff

Browse files
authored
Merge pull request #2204 from iced-rs/vectorial-text-reloaded
Vectorial text reloaded
2 parents 0001a6d + 5d4c55c commit 070abff

7 files changed

Lines changed: 512 additions & 127 deletions

File tree

examples/vectorial_text/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "vectorial_text"
3+
version = "0.1.0"
4+
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
5+
edition = "2021"
6+
publish = false
7+
8+
[dependencies]
9+
iced = { path = "../..", features = ["canvas", "debug"] }
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
use iced::alignment::{self, Alignment};
2+
use iced::mouse;
3+
use iced::widget::{
4+
canvas, checkbox, column, horizontal_space, row, slider, text,
5+
};
6+
use iced::{
7+
Element, Length, Point, Rectangle, Renderer, Sandbox, Settings, Theme,
8+
Vector,
9+
};
10+
11+
pub fn main() -> iced::Result {
12+
VectorialText::run(Settings {
13+
antialiasing: true,
14+
..Settings::default()
15+
})
16+
}
17+
18+
struct VectorialText {
19+
state: State,
20+
}
21+
22+
#[derive(Debug, Clone, Copy)]
23+
enum Message {
24+
SizeChanged(f32),
25+
AngleChanged(f32),
26+
ScaleChanged(f32),
27+
ToggleJapanese(bool),
28+
}
29+
30+
impl Sandbox for VectorialText {
31+
type Message = Message;
32+
33+
fn new() -> Self {
34+
Self {
35+
state: State::new(),
36+
}
37+
}
38+
39+
fn title(&self) -> String {
40+
String::from("Vectorial Text - Iced")
41+
}
42+
43+
fn update(&mut self, message: Message) {
44+
match message {
45+
Message::SizeChanged(size) => {
46+
self.state.size = size;
47+
}
48+
Message::AngleChanged(angle) => {
49+
self.state.angle = angle;
50+
}
51+
Message::ScaleChanged(scale) => {
52+
self.state.scale = scale;
53+
}
54+
Message::ToggleJapanese(use_japanese) => {
55+
self.state.use_japanese = use_japanese;
56+
}
57+
}
58+
59+
self.state.cache.clear();
60+
}
61+
62+
fn view(&self) -> Element<Message> {
63+
let slider_with_label = |label, range, value, message: fn(f32) -> _| {
64+
column![
65+
row![
66+
text(label),
67+
horizontal_space(Length::Fill),
68+
text(format!("{:.2}", value))
69+
],
70+
slider(range, value, message).step(0.01)
71+
]
72+
.spacing(2)
73+
};
74+
75+
column![
76+
canvas(&self.state).width(Length::Fill).height(Length::Fill),
77+
column![
78+
checkbox(
79+
"Use Japanese",
80+
self.state.use_japanese,
81+
Message::ToggleJapanese
82+
),
83+
row![
84+
slider_with_label(
85+
"Size",
86+
2.0..=80.0,
87+
self.state.size,
88+
Message::SizeChanged,
89+
),
90+
slider_with_label(
91+
"Angle",
92+
0.0..=360.0,
93+
self.state.angle,
94+
Message::AngleChanged,
95+
),
96+
slider_with_label(
97+
"Scale",
98+
1.0..=20.0,
99+
self.state.scale,
100+
Message::ScaleChanged,
101+
),
102+
]
103+
.spacing(20),
104+
]
105+
.align_items(Alignment::Center)
106+
.spacing(10)
107+
]
108+
.spacing(10)
109+
.padding(20)
110+
.into()
111+
}
112+
113+
fn theme(&self) -> Theme {
114+
Theme::Dark
115+
}
116+
}
117+
118+
struct State {
119+
size: f32,
120+
angle: f32,
121+
scale: f32,
122+
use_japanese: bool,
123+
cache: canvas::Cache,
124+
}
125+
126+
impl State {
127+
pub fn new() -> Self {
128+
Self {
129+
size: 40.0,
130+
angle: 0.0,
131+
scale: 1.0,
132+
use_japanese: false,
133+
cache: canvas::Cache::new(),
134+
}
135+
}
136+
}
137+
138+
impl<Message> canvas::Program<Message> for State {
139+
type State = ();
140+
141+
fn draw(
142+
&self,
143+
_state: &Self::State,
144+
renderer: &Renderer,
145+
theme: &Theme,
146+
bounds: Rectangle,
147+
_cursor: mouse::Cursor,
148+
) -> Vec<canvas::Geometry> {
149+
let geometry = self.cache.draw(renderer, bounds.size(), |frame| {
150+
let palette = theme.palette();
151+
let center = bounds.center();
152+
153+
frame.translate(Vector::new(center.x, center.y));
154+
frame.scale(self.scale);
155+
frame.rotate(self.angle * std::f32::consts::PI / 180.0);
156+
157+
frame.fill_text(canvas::Text {
158+
position: Point::new(0.0, 0.0),
159+
color: palette.text,
160+
size: self.size.into(),
161+
content: String::from(if self.use_japanese {
162+
"ベクトルテキスト🎉"
163+
} else {
164+
"Vectorial Text! 🎉"
165+
}),
166+
horizontal_alignment: alignment::Horizontal::Center,
167+
vertical_alignment: alignment::Vertical::Center,
168+
shaping: text::Shaping::Advanced,
169+
..canvas::Text::default()
170+
});
171+
});
172+
173+
vec![geometry]
174+
}
175+
}

graphics/src/geometry/text.rs

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::core::alignment;
22
use crate::core::text::{LineHeight, Shaping};
3-
use crate::core::{Color, Font, Pixels, Point};
3+
use crate::core::{Color, Font, Pixels, Point, Size, Vector};
4+
use crate::geometry::Path;
5+
use crate::text;
46

57
/// A bunch of text that can be drawn to a canvas
68
#[derive(Debug, Clone)]
@@ -32,6 +34,137 @@ pub struct Text {
3234
pub shaping: Shaping,
3335
}
3436

37+
impl Text {
38+
/// Computes the [`Path`]s of the [`Text`] and draws them using
39+
/// the given closure.
40+
pub fn draw_with(&self, mut f: impl FnMut(Path, Color)) {
41+
let mut font_system =
42+
text::font_system().write().expect("Write font system");
43+
44+
let mut buffer = cosmic_text::BufferLine::new(
45+
&self.content,
46+
cosmic_text::AttrsList::new(text::to_attributes(self.font)),
47+
text::to_shaping(self.shaping),
48+
);
49+
50+
let layout = buffer.layout(
51+
font_system.raw(),
52+
self.size.0,
53+
f32::MAX,
54+
cosmic_text::Wrap::None,
55+
);
56+
57+
let translation_x = match self.horizontal_alignment {
58+
alignment::Horizontal::Left => self.position.x,
59+
alignment::Horizontal::Center | alignment::Horizontal::Right => {
60+
let mut line_width = 0.0f32;
61+
62+
for line in layout.iter() {
63+
line_width = line_width.max(line.w);
64+
}
65+
66+
if self.horizontal_alignment == alignment::Horizontal::Center {
67+
self.position.x - line_width / 2.0
68+
} else {
69+
self.position.x - line_width
70+
}
71+
}
72+
};
73+
74+
let translation_y = {
75+
let line_height = self.line_height.to_absolute(self.size);
76+
77+
match self.vertical_alignment {
78+
alignment::Vertical::Top => self.position.y,
79+
alignment::Vertical::Center => {
80+
self.position.y - line_height.0 / 2.0
81+
}
82+
alignment::Vertical::Bottom => self.position.y - line_height.0,
83+
}
84+
};
85+
86+
let mut swash_cache = cosmic_text::SwashCache::new();
87+
88+
for run in layout.iter() {
89+
for glyph in run.glyphs.iter() {
90+
let physical_glyph = glyph.physical((0.0, 0.0), 1.0);
91+
92+
let start_x = translation_x + glyph.x + glyph.x_offset;
93+
let start_y = translation_y + glyph.y_offset + self.size.0;
94+
let offset = Vector::new(start_x, start_y);
95+
96+
if let Some(commands) = swash_cache.get_outline_commands(
97+
font_system.raw(),
98+
physical_glyph.cache_key,
99+
) {
100+
let glyph = Path::new(|path| {
101+
use cosmic_text::Command;
102+
103+
for command in commands {
104+
match command {
105+
Command::MoveTo(p) => {
106+
path.move_to(
107+
Point::new(p.x, -p.y) + offset,
108+
);
109+
}
110+
Command::LineTo(p) => {
111+
path.line_to(
112+
Point::new(p.x, -p.y) + offset,
113+
);
114+
}
115+
Command::CurveTo(control_a, control_b, to) => {
116+
path.bezier_curve_to(
117+
Point::new(control_a.x, -control_a.y)
118+
+ offset,
119+
Point::new(control_b.x, -control_b.y)
120+
+ offset,
121+
Point::new(to.x, -to.y) + offset,
122+
);
123+
}
124+
Command::QuadTo(control, to) => {
125+
path.quadratic_curve_to(
126+
Point::new(control.x, -control.y)
127+
+ offset,
128+
Point::new(to.x, -to.y) + offset,
129+
);
130+
}
131+
Command::Close => {
132+
path.close();
133+
}
134+
}
135+
}
136+
});
137+
138+
f(glyph, self.color);
139+
} else {
140+
// TODO: Raster image support for `Canvas`
141+
let [r, g, b, a] = self.color.into_rgba8();
142+
143+
swash_cache.with_pixels(
144+
font_system.raw(),
145+
physical_glyph.cache_key,
146+
cosmic_text::Color::rgba(r, g, b, a),
147+
|x, y, color| {
148+
f(
149+
Path::rectangle(
150+
Point::new(x as f32, y as f32) + offset,
151+
Size::new(1.0, 1.0),
152+
),
153+
Color::from_rgba8(
154+
color.r(),
155+
color.g(),
156+
color.b(),
157+
color.a() as f32 / 255.0,
158+
),
159+
);
160+
},
161+
);
162+
}
163+
}
164+
}
165+
}
166+
}
167+
35168
impl Default for Text {
36169
fn default() -> Text {
37170
Text {

tiny_skia/src/backend.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,6 @@ impl Backend {
543543
path,
544544
paint,
545545
rule,
546-
transform,
547546
}) => {
548547
let bounds = path.bounds();
549548

@@ -566,17 +565,18 @@ impl Backend {
566565
path,
567566
paint,
568567
*rule,
569-
transform
570-
.post_translate(translation.x, translation.y)
571-
.post_scale(scale_factor, scale_factor),
568+
tiny_skia::Transform::from_translate(
569+
translation.x,
570+
translation.y,
571+
)
572+
.post_scale(scale_factor, scale_factor),
572573
clip_mask,
573574
);
574575
}
575576
Primitive::Custom(primitive::Custom::Stroke {
576577
path,
577578
paint,
578579
stroke,
579-
transform,
580580
}) => {
581581
let bounds = path.bounds();
582582

@@ -599,9 +599,11 @@ impl Backend {
599599
path,
600600
paint,
601601
stroke,
602-
transform
603-
.post_translate(translation.x, translation.y)
604-
.post_scale(scale_factor, scale_factor),
602+
tiny_skia::Transform::from_translate(
603+
translation.x,
604+
translation.y,
605+
)
606+
.post_scale(scale_factor, scale_factor),
605607
clip_mask,
606608
);
607609
}

0 commit comments

Comments
 (0)