mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 14:38:11 +08:00
Implement the Text Tool and text layer MVP (#492)
* Add text tool * Double click with the select tool to edit text * Fix (I think?) transitioning to select tool * Commit and abort text editing * Transition to a contenteditable div and autosize * Fix right click blocking * Cleanup hints * Ctrl + enter leaves text edit mode * Render indervidual bounding boxes for text * Re-format space indents * Reflect font size in the textarea * Fix change tool behaviour * Remove starting text * Populate the cache (caused doc load bug) * Remove console log * Chrome display the flashing text entry cursor * Update overlay on input * Cleanup input.ts * Fix bounding boxes * Apply review feedback * Remove manual test * Remove svg from gitignore Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
0da8dabba9
commit
a66920aa1c
+38
-11
@@ -4,6 +4,7 @@ use crate::layers::folder::Folder;
|
||||
use crate::layers::layer_info::{Layer, LayerData, LayerDataType};
|
||||
use crate::layers::simple_shape::Shape;
|
||||
use crate::layers::style::ViewMode;
|
||||
use crate::layers::text::Text;
|
||||
use crate::{DocumentError, DocumentResponse, Operation};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
@@ -113,7 +114,7 @@ impl Document {
|
||||
|
||||
Ok(match self.layer(common_prefix_of_path)?.data {
|
||||
LayerDataType::Folder(_) => common_prefix_of_path,
|
||||
LayerDataType::Shape(_) => &common_prefix_of_path[..common_prefix_of_path.len() - 1],
|
||||
_ => &common_prefix_of_path[..common_prefix_of_path.len() - 1],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -256,6 +257,7 @@ impl Document {
|
||||
}
|
||||
}
|
||||
}
|
||||
LayerDataType::Text(_) => layer.cache_dirty = true,
|
||||
}
|
||||
layer.cache_dirty
|
||||
}
|
||||
@@ -452,6 +454,32 @@ impl Document {
|
||||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat())
|
||||
}
|
||||
Operation::AddText {
|
||||
path,
|
||||
insert_index,
|
||||
transform,
|
||||
text,
|
||||
|
||||
style,
|
||||
size,
|
||||
} => {
|
||||
let layer = Layer::new(LayerDataType::Text(Text::new(text.clone(), *style, *size)), *transform);
|
||||
|
||||
self.set_layer(path, layer, *insert_index)?;
|
||||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::SetTextEditability { path, editable } => {
|
||||
self.layer_mut(path)?.as_text_mut()?.editable = *editable;
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentChanged])
|
||||
}
|
||||
Operation::SetTextContent { path, new_text } => {
|
||||
self.layer_mut(path)?.as_text_mut()?.update_text(new_text.clone());
|
||||
self.mark_as_dirty(path)?;
|
||||
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::AddNgon {
|
||||
path,
|
||||
insert_index,
|
||||
@@ -587,11 +615,8 @@ impl Document {
|
||||
Operation::SetShapePath { path, bez_path } => {
|
||||
self.mark_as_dirty(path)?;
|
||||
|
||||
match &mut self.layer_mut(path)?.data {
|
||||
LayerDataType::Shape(shape) => {
|
||||
shape.path = bez_path.clone();
|
||||
}
|
||||
LayerDataType::Folder(_) => (),
|
||||
if let LayerDataType::Shape(shape) = &mut self.layer_mut(path)?.data {
|
||||
shape.path = bez_path.clone();
|
||||
}
|
||||
Some(vec![DocumentChanged, LayerChanged { path: path.clone() }])
|
||||
}
|
||||
@@ -600,11 +625,13 @@ impl Document {
|
||||
self.set_transform_relative_to_viewport(path, transform)?;
|
||||
self.mark_as_dirty(path)?;
|
||||
|
||||
match &mut self.layer_mut(path)?.data {
|
||||
LayerDataType::Shape(shape) => {
|
||||
shape.path = bez_path.clone();
|
||||
}
|
||||
LayerDataType::Folder(_) => (),
|
||||
if let LayerDataType::Text(t) = &mut self.layer_mut(path)?.data {
|
||||
let bezpath = t.to_bez_path();
|
||||
self.layer_mut(path)?.data = layers::layer_info::LayerDataType::Shape(Shape::from_bez_path(bezpath, t.style, true));
|
||||
}
|
||||
|
||||
if let LayerDataType::Shape(shape) = &mut self.layer_mut(path)?.data {
|
||||
shape.path = bez_path.clone();
|
||||
}
|
||||
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
|
||||
@@ -8,5 +8,6 @@ pub enum DocumentError {
|
||||
NotAFolder,
|
||||
NonReorderableSelection,
|
||||
NotAShape,
|
||||
NotText,
|
||||
InvalidFile(String),
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::blend_mode::BlendMode;
|
||||
use super::folder::Folder;
|
||||
use super::simple_shape::Shape;
|
||||
use super::style::ViewMode;
|
||||
use super::text::Text;
|
||||
use crate::intersection::Quad;
|
||||
use crate::DocumentError;
|
||||
use crate::LayerId;
|
||||
@@ -14,6 +15,7 @@ use std::fmt::Write;
|
||||
pub enum LayerDataType {
|
||||
Folder(Folder),
|
||||
Shape(Shape),
|
||||
Text(Text),
|
||||
}
|
||||
|
||||
impl LayerDataType {
|
||||
@@ -21,6 +23,7 @@ impl LayerDataType {
|
||||
match self {
|
||||
LayerDataType::Shape(s) => s,
|
||||
LayerDataType::Folder(f) => f,
|
||||
LayerDataType::Text(t) => t,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +31,7 @@ impl LayerDataType {
|
||||
match self {
|
||||
LayerDataType::Shape(s) => s,
|
||||
LayerDataType::Folder(f) => f,
|
||||
LayerDataType::Text(t) => t,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,6 +162,20 @@ impl Layer {
|
||||
_ => Err(DocumentError::NotAFolder),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_text_mut(&mut self) -> Result<&mut Text, DocumentError> {
|
||||
match &mut self.data {
|
||||
LayerDataType::Text(t) => Ok(t),
|
||||
_ => Err(DocumentError::NotText),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_text(&self) -> Result<&Text, DocumentError> {
|
||||
match &self.data {
|
||||
LayerDataType::Text(t) => Ok(t),
|
||||
_ => Err(DocumentError::NotText),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Layer {
|
||||
|
||||
@@ -3,3 +3,4 @@ pub mod folder;
|
||||
pub mod layer_info;
|
||||
pub mod simple_shape;
|
||||
pub mod style;
|
||||
pub mod text;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name ‘Source’.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
@@ -0,0 +1,146 @@
|
||||
use super::layer_info::LayerData;
|
||||
use super::style::{self, PathStyle, ViewMode};
|
||||
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||
use crate::LayerId;
|
||||
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use kurbo::{Affine, BezPath, Rect, Shape};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
mod to_kurbo;
|
||||
|
||||
fn glam_to_kurbo(transform: DAffine2) -> Affine {
|
||||
Affine::new(transform.to_cols_array())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Text {
|
||||
pub text: String,
|
||||
pub style: style::PathStyle,
|
||||
pub size: f64,
|
||||
pub line_width: Option<f64>,
|
||||
#[serde(skip)]
|
||||
pub editable: bool,
|
||||
#[serde(skip)]
|
||||
cached_path: Option<BezPath>,
|
||||
}
|
||||
|
||||
impl LayerData for Text {
|
||||
fn render(&mut self, svg: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode) {
|
||||
let transform = self.transform(transforms, view_mode);
|
||||
let inverse = transform.inverse();
|
||||
if !inverse.is_finite() {
|
||||
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||
return;
|
||||
}
|
||||
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
|
||||
let _ = svg.write_str(&(entry.to_string() + if i == 5 { "" } else { "," }));
|
||||
});
|
||||
let _ = svg.write_str(r#")">"#);
|
||||
|
||||
if self.editable {
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<foreignObject transform="matrix({})"></foreignObject>"#,
|
||||
transform
|
||||
.to_cols_array()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, entry)| { entry.to_string() + if i == 5 { "" } else { "," } })
|
||||
.collect::<String>(),
|
||||
);
|
||||
} else {
|
||||
let mut path = self.to_bez_path();
|
||||
|
||||
path.apply_affine(glam_to_kurbo(transform));
|
||||
|
||||
let _ = write!(svg, r#"<path d="{}" {} />"#, path.to_svg(), self.style.render(view_mode));
|
||||
}
|
||||
let _ = svg.write_str("</g>");
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
let mut path = self.bounding_box(&self.text).to_path(0.1);
|
||||
|
||||
if transform.matrix2 == DMat2::ZERO {
|
||||
return None;
|
||||
}
|
||||
path.apply_affine(glam_to_kurbo(transform));
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
Some([(x0, y0).into(), (x1, y1).into()])
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
if intersect_quad_bez_path(quad, &self.bounding_box(&self.text).to_path(0.), true) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
|
||||
let start = match mode {
|
||||
ViewMode::Outline => 0,
|
||||
_ => (transforms.len() as i32 - 1).max(0) as usize,
|
||||
};
|
||||
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
|
||||
}
|
||||
|
||||
pub fn new(text: String, style: PathStyle, size: f64) -> Self {
|
||||
let mut new = Self {
|
||||
text,
|
||||
style,
|
||||
size,
|
||||
line_width: None,
|
||||
editable: false,
|
||||
cached_path: None,
|
||||
};
|
||||
|
||||
new.regenerate_path();
|
||||
|
||||
new
|
||||
}
|
||||
|
||||
/// Converts to a BezPath, populating the cache if necessary
|
||||
#[inline]
|
||||
pub fn to_bez_path(&mut self) -> BezPath {
|
||||
if self.cached_path.is_none() {
|
||||
self.regenerate_path();
|
||||
}
|
||||
self.cached_path.clone().unwrap()
|
||||
}
|
||||
|
||||
/// Converts to a bezpath, without populating the cache
|
||||
#[inline]
|
||||
pub fn to_bez_path_nonmut(&self) -> BezPath {
|
||||
self.cached_path.clone().unwrap_or_else(|| self.generate_path())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn font_face() -> rustybuzz::Face<'static> {
|
||||
rustybuzz::Face::from_slice(include_bytes!("SourceSansPro/SourceSansPro-Regular.ttf"), 0).unwrap()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn generate_path(&self) -> BezPath {
|
||||
to_kurbo::to_kurbo(&self.text, Self::font_face(), self.size, self.line_width)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn bounding_box(&self, text: &str) -> Rect {
|
||||
let far = to_kurbo::bounding_box(text, Self::font_face(), self.size, self.line_width);
|
||||
Rect::new(0., 0., far.x, far.y)
|
||||
}
|
||||
|
||||
pub fn regenerate_path(&mut self) {
|
||||
self.cached_path = Some(self.generate_path());
|
||||
}
|
||||
|
||||
pub fn update_text(&mut self, text: String) {
|
||||
self.text = text;
|
||||
self.regenerate_path();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
use glam::DVec2;
|
||||
use kurbo::{BezPath, Point, Vec2};
|
||||
use rustybuzz::{GlyphBuffer, UnicodeBuffer};
|
||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||
|
||||
struct Builder {
|
||||
path: BezPath,
|
||||
pos: Point,
|
||||
offset: Vec2,
|
||||
ascender: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl OutlineBuilder for Builder {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
self.path.move_to(self.pos + self.offset + Vec2::new(x as f64, self.ascender - y as f64) * self.scale);
|
||||
}
|
||||
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
self.path.line_to(self.pos + self.offset + Vec2::new(x as f64, self.ascender - y as f64) * self.scale);
|
||||
}
|
||||
|
||||
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||
self.path.quad_to(
|
||||
self.pos + self.offset + Vec2::new(x1 as f64, self.ascender - y1 as f64) * self.scale,
|
||||
self.pos + self.offset + Vec2::new(x2 as f64, self.ascender - y2 as f64) * self.scale,
|
||||
);
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
|
||||
self.path.curve_to(
|
||||
self.pos + self.offset + Vec2::new(x1 as f64, self.ascender - y1 as f64) * self.scale,
|
||||
self.pos + self.offset + Vec2::new(x2 as f64, self.ascender - y2 as f64) * self.scale,
|
||||
self.pos + self.offset + Vec2::new(x3 as f64, self.ascender - y3 as f64) * self.scale,
|
||||
);
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.path.close_path();
|
||||
}
|
||||
}
|
||||
|
||||
fn font_properties(buzz_face: &rustybuzz::Face, font_size: f64) -> (f64, f64, UnicodeBuffer) {
|
||||
let scale = (buzz_face.units_per_em() as f64).recip() * font_size;
|
||||
let line_height = font_size;
|
||||
let buffer = UnicodeBuffer::new();
|
||||
(scale, line_height, buffer)
|
||||
}
|
||||
|
||||
fn push_str(buffer: &mut UnicodeBuffer, word: &str, trailing_space: bool) {
|
||||
buffer.push_str(word);
|
||||
|
||||
if trailing_space {
|
||||
buffer.push_str(" ");
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_word(line_width: Option<f64>, glyph_buffer: &GlyphBuffer, scale: f64, x_pos: f64) -> bool {
|
||||
if let Some(line_width) = line_width {
|
||||
let word_length: i32 = glyph_buffer.glyph_positions().iter().map(|pos| pos.x_advance).sum();
|
||||
let scaled_word_length = word_length as f64 * scale;
|
||||
|
||||
if scaled_word_length + x_pos > line_width {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn to_kurbo(str: &str, buzz_face: rustybuzz::Face, font_size: f64, line_width: Option<f64>) -> BezPath {
|
||||
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size);
|
||||
|
||||
let mut builder = Builder {
|
||||
path: BezPath::new(),
|
||||
pos: Point::ZERO,
|
||||
offset: Vec2::ZERO,
|
||||
ascender: buzz_face.ascender() as f64,
|
||||
scale,
|
||||
};
|
||||
|
||||
for line in str.split('\n') {
|
||||
let length = line.split(' ').count();
|
||||
for (index, word) in line.split(' ').enumerate() {
|
||||
push_str(&mut buffer, word, index != length - 1);
|
||||
|
||||
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
|
||||
|
||||
if wrap_word(line_width, &glyph_buffer, scale, builder.pos.x) {
|
||||
builder.pos = Point::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
|
||||
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
|
||||
if let Some(line_width) = line_width {
|
||||
if builder.pos.x + (glyph_position.x_advance as f64 * builder.scale) >= line_width {
|
||||
builder.pos = Point::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
}
|
||||
builder.offset = Vec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) * builder.scale;
|
||||
buzz_face.outline_glyph(GlyphId(glyph_info.glyph_id as u16), &mut builder);
|
||||
builder.pos += Vec2::new(glyph_position.x_advance as f64, glyph_position.y_advance as f64) * builder.scale;
|
||||
}
|
||||
|
||||
buffer = glyph_buffer.clear();
|
||||
}
|
||||
builder.pos = Point::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
builder.path
|
||||
}
|
||||
|
||||
pub fn bounding_box(str: &str, buzz_face: rustybuzz::Face, font_size: f64, line_width: Option<f64>) -> DVec2 {
|
||||
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size);
|
||||
|
||||
let mut pos = DVec2::ZERO;
|
||||
let mut bounds = DVec2::ZERO;
|
||||
|
||||
for line in str.split('\n') {
|
||||
let length = line.split(' ').count();
|
||||
for (index, word) in line.split(' ').enumerate() {
|
||||
push_str(&mut buffer, word, index != length - 1);
|
||||
|
||||
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
|
||||
|
||||
if wrap_word(line_width, &glyph_buffer, scale, pos.x) {
|
||||
pos = DVec2::new(0., pos.y + line_height);
|
||||
}
|
||||
|
||||
for glyph_position in glyph_buffer.glyph_positions() {
|
||||
if let Some(line_width) = line_width {
|
||||
if pos.x + (glyph_position.x_advance as f64 * scale) >= line_width {
|
||||
pos = DVec2::new(0., pos.y + line_height);
|
||||
}
|
||||
}
|
||||
pos += DVec2::new(glyph_position.x_advance as f64, glyph_position.y_advance as f64) * scale;
|
||||
}
|
||||
bounds = bounds.max(pos + DVec2::new(0., line_height));
|
||||
|
||||
buffer = glyph_buffer.clear();
|
||||
}
|
||||
pos = DVec2::new(0., pos.y + line_height);
|
||||
}
|
||||
|
||||
bounds
|
||||
}
|
||||
@@ -45,6 +45,22 @@ pub enum Operation {
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddText {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
insert_index: isize,
|
||||
text: String,
|
||||
style: style::PathStyle,
|
||||
size: f64,
|
||||
},
|
||||
SetTextEditability {
|
||||
path: Vec<LayerId>,
|
||||
editable: bool,
|
||||
},
|
||||
SetTextContent {
|
||||
path: Vec<LayerId>,
|
||||
new_text: String,
|
||||
},
|
||||
AddPolyline {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
|
||||
Reference in New Issue
Block a user