Implement user-named TextStyle:s
This commit is contained in:
parent
4f98f26fda
commit
1aceaefba3
7 changed files with 109 additions and 74 deletions
|
@ -58,7 +58,7 @@ pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Respo
|
||||||
impl Widget for &mut epaint::text::FontDefinitions {
|
impl Widget for &mut epaint::text::FontDefinitions {
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
for (text_style, (_family, size)) in self.family_and_size.iter_mut() {
|
for (text_style, (size, _family)) in self.styles.iter_mut() {
|
||||||
// TODO: radio button for family
|
// TODO: radio button for family
|
||||||
ui.add(
|
ui.add(
|
||||||
Slider::new(size, 4.0..=40.0)
|
Slider::new(size, 4.0..=40.0)
|
||||||
|
|
|
@ -597,7 +597,7 @@ impl Style {
|
||||||
})
|
})
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
ui.selectable_value(override_text_style, None, "None");
|
ui.selectable_value(override_text_style, None, "None");
|
||||||
for style in TextStyle::all() {
|
for style in TextStyle::built_in() {
|
||||||
let text =
|
let text =
|
||||||
crate::RichText::new(format!("{:?}", style)).text_style(style.clone());
|
crate::RichText::new(format!("{:?}", style)).text_style(style.clone());
|
||||||
ui.selectable_value(override_text_style, Some(style), text);
|
ui.selectable_value(override_text_style, Some(style), text);
|
||||||
|
|
|
@ -54,7 +54,7 @@ impl super::View for FontBook {
|
||||||
egui::ComboBox::from_label("Text style")
|
egui::ComboBox::from_label("Text style")
|
||||||
.selected_text(format!("{:?}", self.text_style))
|
.selected_text(format!("{:?}", self.text_style))
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
for style in egui::TextStyle::all() {
|
for style in egui::TextStyle::built_in() {
|
||||||
ui.selectable_value(
|
ui.selectable_value(
|
||||||
&mut self.text_style,
|
&mut self.text_style,
|
||||||
style.clone(),
|
style.clone(),
|
||||||
|
@ -81,7 +81,7 @@ impl super::View for FontBook {
|
||||||
ui.fonts()
|
ui.fonts()
|
||||||
.lock()
|
.lock()
|
||||||
.fonts
|
.fonts
|
||||||
.font_mut(&text_style)
|
.font_for_style(&text_style)
|
||||||
.characters()
|
.characters()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control())
|
.filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control())
|
||||||
|
|
|
@ -261,7 +261,7 @@ impl Widget for &mut LegendDemo {
|
||||||
egui::Grid::new("settings").show(ui, |ui| {
|
egui::Grid::new("settings").show(ui, |ui| {
|
||||||
ui.label("Text style:");
|
ui.label("Text style:");
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
TextStyle::all().for_each(|style| {
|
TextStyle::built_in().for_each(|style| {
|
||||||
ui.selectable_value(
|
ui.selectable_value(
|
||||||
&mut config.text_style,
|
&mut config.text_style,
|
||||||
style.clone(),
|
style.clone(),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
mutex::{Arc, Mutex, RwLock},
|
mutex::{Arc, Mutex, RwLock},
|
||||||
text::TextStyle,
|
|
||||||
TextureAtlas,
|
TextureAtlas,
|
||||||
};
|
};
|
||||||
use ahash::AHashMap;
|
use ahash::AHashMap;
|
||||||
|
@ -195,7 +194,6 @@ type FontIndex = usize;
|
||||||
// TODO: rename?
|
// TODO: rename?
|
||||||
/// Wrapper over multiple `FontImpl` (e.g. a primary + fallbacks for emojis)
|
/// Wrapper over multiple `FontImpl` (e.g. a primary + fallbacks for emojis)
|
||||||
pub struct Font {
|
pub struct Font {
|
||||||
text_style: TextStyle,
|
|
||||||
fonts: Vec<Arc<FontImpl>>,
|
fonts: Vec<Arc<FontImpl>>,
|
||||||
/// Lazily calculated.
|
/// Lazily calculated.
|
||||||
characters: Option<std::collections::BTreeSet<char>>,
|
characters: Option<std::collections::BTreeSet<char>>,
|
||||||
|
@ -206,10 +204,9 @@ pub struct Font {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Font {
|
impl Font {
|
||||||
pub fn new(text_style: TextStyle, fonts: Vec<Arc<FontImpl>>) -> Self {
|
pub fn new(fonts: Vec<Arc<FontImpl>>) -> Self {
|
||||||
if fonts.is_empty() {
|
if fonts.is_empty() {
|
||||||
return Self {
|
return Self {
|
||||||
text_style,
|
|
||||||
fonts,
|
fonts,
|
||||||
characters: None,
|
characters: None,
|
||||||
replacement_glyph: Default::default(),
|
replacement_glyph: Default::default(),
|
||||||
|
@ -223,7 +220,6 @@ impl Font {
|
||||||
let row_height = fonts[0].row_height();
|
let row_height = fonts[0].row_height();
|
||||||
|
|
||||||
let mut slf = Self {
|
let mut slf = Self {
|
||||||
text_style,
|
|
||||||
fonts,
|
fonts,
|
||||||
characters: None,
|
characters: None,
|
||||||
replacement_glyph: Default::default(),
|
replacement_glyph: Default::default(),
|
||||||
|
@ -269,11 +265,6 @@ impl Font {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn text_style(&self) -> &TextStyle {
|
|
||||||
&self.text_style
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn round_to_pixel(&self, point: f32) -> f32 {
|
pub fn round_to_pixel(&self, point: f32) -> f32 {
|
||||||
(point * self.pixels_per_point).round() / self.pixels_per_point
|
(point * self.pixels_per_point).round() / self.pixels_per_point
|
||||||
|
|
|
@ -9,55 +9,81 @@ use crate::{
|
||||||
TextureAtlas,
|
TextureAtlas,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: rename
|
/// Font of unknown size.
|
||||||
|
///
|
||||||
|
/// Which style of font: [`Monospace`][`FontFamily::Monospace`], [`Proportional`][`FontFamily::Proportional`],
|
||||||
|
/// or by user-chosen name.
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
||||||
|
pub enum FontFamily {
|
||||||
|
/// A font where each character is the same width (`w` is the same width as `i`).
|
||||||
|
Monospace,
|
||||||
|
|
||||||
|
/// A font where some characters are wider than other (e.g. 'w' is wider than 'i').
|
||||||
|
Proportional,
|
||||||
|
|
||||||
|
/// ```
|
||||||
|
/// // User-chosen names:
|
||||||
|
/// FontFamily::Name("arial".into());
|
||||||
|
/// FontFamily::Name("serif".into());
|
||||||
|
/// ```
|
||||||
|
Name(Arc<str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alias for a font of known size.
|
||||||
|
///
|
||||||
/// One of a few categories of styles of text, e.g. body, button or heading.
|
/// One of a few categories of styles of text, e.g. body, button or heading.
|
||||||
|
/// Useful in GUI:s.
|
||||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
||||||
pub enum TextStyle {
|
pub enum TextStyle {
|
||||||
/// Used when small text is needed.
|
/// Used when small text is needed.
|
||||||
Small,
|
Small,
|
||||||
|
|
||||||
/// Normal labels. Easily readable, doesn't take up too much space.
|
/// Normal labels. Easily readable, doesn't take up too much space.
|
||||||
Body,
|
Body,
|
||||||
/// Buttons. Maybe slightly bigger than `Body`.
|
|
||||||
Button,
|
/// Same size as [`Self::Body]`, but used when monospace is important (for aligning number, code snippets, etc).
|
||||||
/// Heading. Probably larger than `Body`.
|
|
||||||
Heading,
|
|
||||||
/// Same size as `Body`, but used when monospace is important (for aligning number, code snippets, etc).
|
|
||||||
Monospace,
|
Monospace,
|
||||||
|
|
||||||
|
/// Buttons. Maybe slightly bigger than [`Self::Body]`.
|
||||||
|
/// Signifies that he item is interactive.
|
||||||
|
Button,
|
||||||
|
|
||||||
|
/// Heading. Probably larger than [`Self::Body]`.
|
||||||
|
Heading,
|
||||||
|
|
||||||
|
/// ```
|
||||||
|
/// // A user-chosen name of a style:
|
||||||
|
/// TextStyle::Name("footing".into());
|
||||||
|
/// ````
|
||||||
|
Name(Arc<str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextStyle {
|
impl TextStyle {
|
||||||
pub fn all() -> impl ExactSizeIterator<Item = TextStyle> {
|
/// All default (un-named) `TextStyle`:s.
|
||||||
|
pub fn built_in() -> impl ExactSizeIterator<Item = TextStyle> {
|
||||||
[
|
[
|
||||||
TextStyle::Small,
|
TextStyle::Small,
|
||||||
TextStyle::Body,
|
TextStyle::Body,
|
||||||
|
TextStyle::Monospace,
|
||||||
TextStyle::Button,
|
TextStyle::Button,
|
||||||
TextStyle::Heading,
|
TextStyle::Heading,
|
||||||
TextStyle::Monospace,
|
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which style of font: [`Monospace`][`FontFamily::Monospace`] or [`Proportional`][`FontFamily::Proportional`].
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
||||||
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
|
||||||
pub enum FontFamily {
|
|
||||||
/// A font where each character is the same width (`w` is the same width as `i`).
|
|
||||||
Monospace,
|
|
||||||
/// A font where some characters are wider than other (e.g. 'w' is wider than 'i').
|
|
||||||
Proportional,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A `.ttf` or `.otf` file and a font face index.
|
/// A `.ttf` or `.otf` file and a font face index.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct FontData {
|
pub struct FontData {
|
||||||
/// The content of a `.ttf` or `.otf` file.
|
/// The content of a `.ttf` or `.otf` file.
|
||||||
pub font: std::borrow::Cow<'static, [u8]>,
|
pub font: std::borrow::Cow<'static, [u8]>,
|
||||||
|
|
||||||
/// Which font face in the file to use.
|
/// Which font face in the file to use.
|
||||||
/// When in doubt, use `0`.
|
/// When in doubt, use `0`.
|
||||||
pub index: u32,
|
pub index: u32,
|
||||||
|
@ -105,7 +131,7 @@ fn ab_glyph_font_from_font_data(name: &str, data: &FontData) -> ab_glyph::FontAr
|
||||||
/// let mut fonts = FontDefinitions::default();
|
/// let mut fonts = FontDefinitions::default();
|
||||||
///
|
///
|
||||||
/// // Large button text:
|
/// // Large button text:
|
||||||
/// fonts.family_and_size.insert(
|
/// fonts.styles.insert(
|
||||||
/// TextStyle::Button,
|
/// TextStyle::Button,
|
||||||
/// (FontFamily::Proportional, 32.0)
|
/// (FontFamily::Proportional, 32.0)
|
||||||
/// );
|
/// );
|
||||||
|
@ -150,10 +176,11 @@ pub struct FontDefinitions {
|
||||||
/// When looking for a character glyph `epaint` will start with
|
/// When looking for a character glyph `epaint` will start with
|
||||||
/// the first font and then move to the second, and so on.
|
/// the first font and then move to the second, and so on.
|
||||||
/// So the first font is the primary, and then comes a list of fallbacks in order of priority.
|
/// So the first font is the primary, and then comes a list of fallbacks in order of priority.
|
||||||
|
// TODO: per font size-modifier.
|
||||||
pub fonts_for_family: BTreeMap<FontFamily, Vec<String>>,
|
pub fonts_for_family: BTreeMap<FontFamily, Vec<String>>,
|
||||||
|
|
||||||
/// The [`FontFamily`] and size you want to use for a specific [`TextStyle`].
|
/// The [`FontFamily`] and size you want to use for a specific [`TextStyle`].
|
||||||
pub family_and_size: BTreeMap<TextStyle, (FontFamily, f32)>,
|
pub styles: BTreeMap<TextStyle, (f32, FontFamily)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FontDefinitions {
|
impl Default for FontDefinitions {
|
||||||
|
@ -210,17 +237,17 @@ impl Default for FontDefinitions {
|
||||||
fonts_for_family.insert(FontFamily::Proportional, vec![]);
|
fonts_for_family.insert(FontFamily::Proportional, vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut family_and_size = BTreeMap::new();
|
let mut styles = BTreeMap::new();
|
||||||
family_and_size.insert(TextStyle::Small, (FontFamily::Proportional, 10.0));
|
styles.insert(TextStyle::Small, (10.0, FontFamily::Proportional));
|
||||||
family_and_size.insert(TextStyle::Body, (FontFamily::Proportional, 14.0));
|
styles.insert(TextStyle::Body, (14.0, FontFamily::Proportional));
|
||||||
family_and_size.insert(TextStyle::Button, (FontFamily::Proportional, 14.0));
|
styles.insert(TextStyle::Button, (14.0, FontFamily::Proportional));
|
||||||
family_and_size.insert(TextStyle::Heading, (FontFamily::Proportional, 20.0));
|
styles.insert(TextStyle::Heading, (20.0, FontFamily::Proportional));
|
||||||
family_and_size.insert(TextStyle::Monospace, (FontFamily::Monospace, 14.0));
|
styles.insert(TextStyle::Monospace, (14.0, FontFamily::Monospace));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
font_data,
|
font_data,
|
||||||
fonts_for_family,
|
fonts_for_family,
|
||||||
family_and_size,
|
styles,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -366,8 +393,9 @@ impl FontsAndCache {
|
||||||
pub struct FontsImpl {
|
pub struct FontsImpl {
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
definitions: FontDefinitions,
|
definitions: FontDefinitions,
|
||||||
fonts: BTreeMap<TextStyle, Font>,
|
|
||||||
atlas: Arc<Mutex<TextureAtlas>>,
|
atlas: Arc<Mutex<TextureAtlas>>,
|
||||||
|
font_impl_cache: FontImplCache,
|
||||||
|
styles: BTreeMap<TextStyle, Font>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FontsImpl {
|
impl FontsImpl {
|
||||||
|
@ -393,31 +421,24 @@ impl FontsImpl {
|
||||||
|
|
||||||
let atlas = Arc::new(Mutex::new(atlas));
|
let atlas = Arc::new(Mutex::new(atlas));
|
||||||
|
|
||||||
let mut font_impl_cache = FontImplCache::new(atlas.clone(), pixels_per_point, &definitions);
|
let font_impl_cache =
|
||||||
|
FontImplCache::new(atlas.clone(), pixels_per_point, &definitions.font_data);
|
||||||
|
|
||||||
let fonts = definitions
|
let mut slf = Self {
|
||||||
.family_and_size
|
|
||||||
.iter()
|
|
||||||
.map(|(text_style, (family, scale_in_points))| {
|
|
||||||
let fonts = &definitions.fonts_for_family.get(family);
|
|
||||||
let fonts = fonts.unwrap_or_else(|| {
|
|
||||||
panic!("FontFamily::{:?} is not bound to any fonts", family)
|
|
||||||
});
|
|
||||||
let fonts: Vec<Arc<FontImpl>> = fonts
|
|
||||||
.iter()
|
|
||||||
.map(|font_name| font_impl_cache.font_impl(font_name, *scale_in_points))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
(text_style.clone(), Font::new(text_style.clone(), fonts))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
definitions,
|
definitions,
|
||||||
fonts,
|
|
||||||
atlas,
|
atlas,
|
||||||
|
font_impl_cache,
|
||||||
|
styles: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// pre-cache all styles:
|
||||||
|
let styles: Vec<TextStyle> = slf.definitions.styles.keys().cloned().collect();
|
||||||
|
for style in styles {
|
||||||
|
slf.font_for_style(&style);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slf
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
|
@ -425,23 +446,47 @@ impl FontsImpl {
|
||||||
self.pixels_per_point
|
self.pixels_per_point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn definitions(&self) -> &FontDefinitions {
|
pub fn definitions(&self) -> &FontDefinitions {
|
||||||
&self.definitions
|
&self.definitions
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
pub fn font_for_style(&mut self, text_style: &TextStyle) -> &mut Font {
|
||||||
pub fn font_mut(&mut self, text_style: &TextStyle) -> &mut Font {
|
self.styles.entry(text_style.clone()).or_insert_with(|| {
|
||||||
self.fonts.get_mut(text_style).unwrap()
|
let (scale_in_points, family) = self
|
||||||
|
.definitions
|
||||||
|
.styles
|
||||||
|
.get(text_style)
|
||||||
|
.or_else(|| {
|
||||||
|
eprintln!(
|
||||||
|
"Missing TextStyle {:?} in font definitions; falling back to Body",
|
||||||
|
text_style
|
||||||
|
);
|
||||||
|
self.definitions.styles.get(&TextStyle::Body)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| panic!("Missing {:?} in font definitions", TextStyle::Body));
|
||||||
|
|
||||||
|
let fonts = &self.definitions.fonts_for_family.get(family);
|
||||||
|
let fonts = fonts
|
||||||
|
.unwrap_or_else(|| panic!("FontFamily::{:?} is not bound to any fonts", family));
|
||||||
|
|
||||||
|
let fonts: Vec<Arc<FontImpl>> = fonts
|
||||||
|
.iter()
|
||||||
|
.map(|font_name| self.font_impl_cache.font_impl(*scale_in_points, font_name))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Font::new(fonts)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Width of this character in points.
|
/// Width of this character in points.
|
||||||
fn glyph_width(&mut self, text_style: &TextStyle, c: char) -> f32 {
|
fn glyph_width(&mut self, text_style: &TextStyle, c: char) -> f32 {
|
||||||
self.font_mut(text_style).glyph_width(c)
|
self.font_for_style(text_style).glyph_width(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Height of one row of text. In points
|
/// Height of one row of text. In points
|
||||||
fn row_height(&mut self, text_style: &TextStyle) -> f32 {
|
fn row_height(&mut self, text_style: &TextStyle) -> f32 {
|
||||||
self.font_mut(text_style).row_height()
|
self.font_for_style(text_style).row_height()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -511,10 +556,9 @@ impl FontImplCache {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
atlas: Arc<Mutex<TextureAtlas>>,
|
atlas: Arc<Mutex<TextureAtlas>>,
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
definitions: &super::FontDefinitions,
|
font_data: &BTreeMap<String, FontData>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let ab_glyph_fonts = definitions
|
let ab_glyph_fonts = font_data
|
||||||
.font_data
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, font_data)| (name.clone(), ab_glyph_font_from_font_data(name, font_data)))
|
.map(|(name, font_data)| (name.clone(), ab_glyph_font_from_font_data(name, font_data)))
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -527,7 +571,7 @@ impl FontImplCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn font_impl(&mut self, font_name: &str, scale_in_points: f32) -> Arc<FontImpl> {
|
pub fn font_impl(&mut self, scale_in_points: f32, font_name: &str) -> Arc<FontImpl> {
|
||||||
let y_offset = if font_name == "emoji-icon-font" {
|
let y_offset = if font_name == "emoji-icon-font" {
|
||||||
scale_in_points * 0.235 // TODO: remove font alignment hack
|
scale_in_points * 0.235 // TODO: remove font alignment hack
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -86,7 +86,7 @@ fn layout_section(
|
||||||
byte_range,
|
byte_range,
|
||||||
format,
|
format,
|
||||||
} = section;
|
} = section;
|
||||||
let font = fonts.font_mut(&format.style);
|
let font = fonts.font_for_style(&format.style);
|
||||||
let font_height = font.row_height();
|
let font_height = font.row_height();
|
||||||
|
|
||||||
let mut paragraph = out_paragraphs.last_mut().unwrap();
|
let mut paragraph = out_paragraphs.last_mut().unwrap();
|
||||||
|
|
Loading…
Reference in a new issue