use std::array;
use std::collections::{hash_map, HashMap};
use std::fmt::{self, Debug};
use std::sync::{Arc, Mutex, PoisonError, Weak};
use cosmic_text::{Attrs, AttrsOwned, LayoutGlyph, SwashContent};
use figures::units::{Lp, Px, UPx};
use figures::{FloatConversion, Fraction, Point, Rect, Round, ScreenScale, Size, UPx2D, Zero};
use intentional::Cast;
use smallvec::SmallVec;
use crate::buffer::Buffer;
use crate::pipeline::PreparedCommand;
use crate::sealed::{ShapeSource, TextureSource};
use crate::{
Assert, CanRenderTo, CollectedTexture, Color, DefaultHasher, DrawableSource, Graphics,
Kludgine, PreparedGraphic, ProtoGraphics, TextureBlit, TextureCollection, VertexCollection,
};
impl Kludgine {
pub fn font_system(&mut self) -> &mut cosmic_text::FontSystem {
&mut self.text.fonts
}
pub fn rebuild_font_system(&mut self) {
let existing_system = std::mem::replace(
&mut self.text.fonts,
cosmic_text::FontSystem::new_with_fonts([]),
);
let (locale, db) = existing_system.into_locale_and_db();
self.text.fonts = cosmic_text::FontSystem::new_with_locale_and_db(locale, db);
}
pub(crate) fn update_scratch_buffer(&mut self, text: &str, width: Option<Px>) {
self.text
.update_scratch_buffer(text, self.effective_scale, width);
}
pub fn set_font_size(&mut self, size: impl figures::ScreenScale<Lp = figures::units::Lp>) {
self.text.set_font_size(
figures::ScreenScale::into_lp(size, self.effective_scale),
self.effective_scale,
);
}
pub fn font_size(&self) -> figures::units::Lp {
self.text.font_size
}
pub fn set_line_height(&mut self, size: impl figures::ScreenScale<Lp = figures::units::Lp>) {
self.text.set_line_height(
figures::ScreenScale::into_lp(size, self.effective_scale),
self.effective_scale,
);
}
pub fn line_height(&self) -> figures::units::Lp {
self.text.line_height
}
pub fn set_font_family(&mut self, family: cosmic_text::FamilyOwned) {
self.text.attrs.family_owned = family;
}
pub fn font_family(&self) -> cosmic_text::Family<'_> {
self.text.attrs.family_owned.as_family()
}
pub fn set_font_style(&mut self, style: cosmic_text::Style) {
self.text.attrs.style = style;
}
pub fn font_style(&self) -> cosmic_text::Style {
self.text.attrs.style
}
pub fn set_font_weight(&mut self, weight: cosmic_text::Weight) {
self.text.attrs.weight = weight;
}
pub fn font_weight(&self) -> cosmic_text::Weight {
self.text.attrs.weight
}
pub fn set_text_stretch(&mut self, width: cosmic_text::Stretch) {
self.text.attrs.stretch = width;
}
pub fn text_stretch(&self) -> cosmic_text::Stretch {
self.text.attrs.stretch
}
pub fn text_attrs(&self) -> cosmic_text::Attrs<'_> {
self.text.attrs.as_attrs()
}
pub fn set_text_attributes(&mut self, attrs: Attrs<'_>) {
self.text.attrs = AttrsOwned::new(attrs);
}
pub fn reset_text_attributes(&mut self) {
self.set_text_attributes(Attrs::new());
self.text.font_size = DEFAULT_FONT_SIZE;
self.text.line_height = DEFAULT_LINE_SIZE;
}
}
pub(crate) struct TextSystem {
pub fonts: cosmic_text::FontSystem,
pub swash_cache: cosmic_text::SwashCache,
pub alpha_text_atlas: TextureCollection,
pub color_text_atlas: TextureCollection,
pub scratch: Option<cosmic_text::Buffer>,
pub font_size: Lp,
pub line_height: Lp,
pub attrs: AttrsOwned,
glyphs: GlyphCache,
}
impl Debug for TextSystem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TextSystem")
.field("font_size", &self.font_size)
.field("line_height", &self.line_height)
.field("attrs", &self.attrs)
.field("glyphs", &self.glyphs)
.finish_non_exhaustive()
}
}
const DEFAULT_FONT_SIZE: Lp = Lp::points(12);
const DEFAULT_LINE_SIZE: Lp = Lp::points(16);
impl TextSystem {
pub(crate) fn new(graphics: &ProtoGraphics<'_>) -> Self {
let fonts = cosmic_text::FontSystem::new();
Self {
alpha_text_atlas: TextureCollection::new_generic(
Size::new(512, 512).cast(),
wgpu::TextureFormat::R8Unorm,
wgpu::FilterMode::Linear,
graphics,
),
color_text_atlas: TextureCollection::new_generic(
Size::new(512, 512).cast(),
wgpu::TextureFormat::Rgba8UnormSrgb,
wgpu::FilterMode::Linear,
graphics,
),
swash_cache: cosmic_text::SwashCache::new(),
scratch: None,
fonts,
font_size: DEFAULT_FONT_SIZE,
line_height: DEFAULT_LINE_SIZE,
glyphs: GlyphCache::default(),
attrs: AttrsOwned::new(Attrs::new()),
}
}
pub fn new_frame(&mut self) {
self.glyphs.clear_unused();
}
fn metrics(&self, scale: Fraction) -> cosmic_text::Metrics {
let font_size = self.font_size.into_px(scale);
let line_height = self.line_height.into_px(scale);
cosmic_text::Metrics::new(font_size.into(), line_height.into())
}
pub fn set_font_size(&mut self, size: Lp, scale: Fraction) {
self.font_size = size;
self.update_buffer_metrics(scale);
}
pub fn set_line_height(&mut self, size: Lp, scale: Fraction) {
self.line_height = size;
self.update_buffer_metrics(scale);
}
pub fn scale_changed(&mut self, scale: Fraction) {
self.update_buffer_metrics(scale);
}
fn update_buffer_metrics(&mut self, scale: Fraction) {
let metrics = self.metrics(scale);
if let Some(buffer) = &mut self.scratch {
buffer.set_metrics(&mut self.fonts, metrics);
}
}
pub fn update_scratch_buffer(&mut self, text: &str, scale: Fraction, width: Option<Px>) {
if self.scratch.is_none() {
let metrics = self.metrics(scale);
let buffer = cosmic_text::Buffer::new(&mut self.fonts, metrics);
self.scratch = Some(buffer);
}
let scratch = self.scratch.as_mut().expect("initialized above");
scratch.set_text(
&mut self.fonts,
text,
self.attrs.as_attrs(),
cosmic_text::Shaping::Advanced, );
scratch.set_size(&mut self.fonts, width.map(Cast::cast), None);
scratch.shape_until_scroll(&mut self.fonts, false);
}
}
#[derive(Debug, Default, Clone)]
struct GlyphCache {
glyphs: Arc<Mutex<HashMap<cosmic_text::CacheKey, CachedGlyph, DefaultHasher>>>,
}
impl GlyphCache {
fn get_or_insert(
&self,
key: cosmic_text::CacheKey,
insert_fn: impl FnOnce() -> Option<(CollectedTexture, bool)>,
) -> Option<CachedGlyphHandle> {
let mut data = self.glyphs.lock().unwrap_or_else(PoisonError::into_inner);
let cached = match data.entry(key) {
hash_map::Entry::Occupied(cached) => {
let cached = cached.into_mut();
cached.ref_count += 1;
cached
}
hash_map::Entry::Vacant(vacant) => {
let (texture, is_mask) = insert_fn()?;
vacant.insert(CachedGlyph {
texture,
is_mask,
ref_count: 1,
})
}
};
Some(CachedGlyphHandle {
key,
is_mask: cached.is_mask,
cache: Arc::downgrade(&self.glyphs),
texture: cached.texture.clone(),
})
}
fn clear_unused(&mut self) {
let mut data = self.glyphs.lock().unwrap_or_else(PoisonError::into_inner);
data.retain(|_, glyph| glyph.ref_count > 0);
}
}
#[derive(Debug)]
struct CachedGlyph {
texture: CollectedTexture,
is_mask: bool,
ref_count: usize,
}
pub(crate) struct CachedGlyphHandle {
key: cosmic_text::CacheKey,
pub is_mask: bool,
cache: Weak<Mutex<HashMap<cosmic_text::CacheKey, CachedGlyph, DefaultHasher>>>,
pub texture: CollectedTexture,
}
impl Debug for CachedGlyphHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CachedGlyphHandle")
.field("key", &self.key)
.field("is_mask", &self.is_mask)
.finish_non_exhaustive()
}
}
impl Clone for CachedGlyphHandle {
fn clone(&self) -> Self {
if let Some(glyphs) = self.cache.upgrade() {
let mut data = glyphs.lock().unwrap_or_else(PoisonError::into_inner);
let cached = data.get_mut(&self.key).expect("cached glyph missing");
cached.ref_count += 1;
drop(data);
}
Self {
key: self.key,
is_mask: self.is_mask,
cache: self.cache.clone(),
texture: self.texture.clone(),
}
}
}
impl Drop for CachedGlyphHandle {
fn drop(&mut self) {
if let Some(glyphs) = self.cache.upgrade() {
let mut data = glyphs.lock().unwrap_or_else(PoisonError::into_inner);
let cached = data.get_mut(&self.key).expect("cached glyph missing");
cached.ref_count -= 1;
}
}
}
impl<'gfx> Graphics<'gfx> {
pub fn prepare_text(
&mut self,
buffer: &cosmic_text::Buffer,
default_color: Color,
origin: TextOrigin<Px>,
) -> PreparedText {
let mut glyphs = HashMap::default();
let mut vertices = VertexCollection::default();
let mut indices = Vec::new();
let mut commands = SmallVec::<[PreparedCommand; 2]>::new();
map_each_glyph(
Some(buffer),
default_color,
origin,
self.kludgine,
self.device,
self.queue,
&mut glyphs,
|blit, _glyph, _is_first_line, _baseline, _line_w, kludgine| {
if let GlyphBlit::Visible {
blit,
glyph: cached,
} = blit
{
let corners: [u32; 4] =
array::from_fn(|index| vertices.get_or_insert(blit.verticies[index]));
let start_index = u32::try_from(indices.len()).assert("too many drawn indices");
for &index in blit.indices() {
indices
.push(corners[usize::try_from(index).assert("too many drawn indices")]);
}
let end_index = u32::try_from(indices.len()).assert("too many drawn indices");
match commands.last_mut() {
Some(last_command) if last_command.is_mask == cached.is_mask => {
last_command.indices.end = end_index;
}
_ => {
commands.push(PreparedCommand {
indices: start_index..end_index,
is_mask: cached.is_mask,
binding: Some(cached.texture.bind_group(&ProtoGraphics::new(
self.device,
self.queue,
kludgine,
))),
});
}
}
}
},
);
PreparedText {
graphic: PreparedGraphic {
vertices: Buffer::new(&vertices.vertices, wgpu::BufferUsages::VERTEX, self.device),
indices: Buffer::new(&indices, wgpu::BufferUsages::INDEX, self.device),
commands,
},
_glyphs: glyphs,
}
}
}
#[allow(clippy::too_many_lines)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn map_each_glyph(
buffer: Option<&cosmic_text::Buffer>,
default_color: Color,
origin: TextOrigin<Px>,
kludgine: &mut Kludgine,
device: &wgpu::Device,
queue: &wgpu::Queue,
glyphs: &mut HashMap<cosmic_text::CacheKey, CachedGlyphHandle, DefaultHasher>,
mut map: impl for<'a> FnMut(GlyphBlit, &'a LayoutGlyph, usize, Px, Px, &'a Kludgine),
) {
let metrics = buffer
.unwrap_or_else(|| kludgine.text.scratch.as_ref().expect("no buffer"))
.metrics();
let line_height_offset = Point::new(Px::ZERO, Px::from(metrics.line_height));
let relative_to = match origin {
TextOrigin::Custom(point) => point,
TextOrigin::TopLeft => Point::default(),
TextOrigin::Center => {
let measured =
measure_text::<Px, false>(buffer, default_color, kludgine, device, queue, glyphs);
(Point::from(measured.size) / 2).round()
}
TextOrigin::FirstBaseline => line_height_offset.cast(),
} + line_height_offset;
let buffer = buffer.unwrap_or_else(|| kludgine.text.scratch.as_ref().expect("no buffer"));
for run in buffer.layout_runs() {
let run_origin = Point::new(Px::ZERO, Px::from(run.line_y)) - relative_to;
for glyph in run.glyphs {
let physical =
glyph.physical((run_origin.x.into_float(), run_origin.y.into_float()), 1.);
let Some(image) = kludgine
.text
.swash_cache
.get_image(&mut kludgine.text.fonts, physical.cache_key)
else {
continue;
};
let invisible = image.placement.width == 0 || image.placement.height == 0;
let mut color = glyph.color_opt.map_or(default_color, Color::from);
let cached = if invisible {
None
} else {
kludgine
.text
.glyphs
.get_or_insert(physical.cache_key, || match image.content {
SwashContent::Mask => Some((
kludgine.text.alpha_text_atlas.push_texture_generic(
&image.data,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(image.placement.width),
rows_per_image: None,
},
Size::upx(image.placement.width, image.placement.height).cast(),
&ProtoGraphics {
id: kludgine.id,
device,
queue,
binding_layout: &kludgine.binding_layout,
linear_sampler: &kludgine.linear_sampler,
nearest_sampler: &kludgine.nearest_sampler,
uniforms: &kludgine.uniforms.wgpu,
},
),
true,
)),
SwashContent::Color => {
color = Color::WHITE;
Some((
kludgine.text.color_text_atlas.push_texture_generic(
&image.data,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(image.placement.width * 4),
rows_per_image: None,
},
Size::upx(image.placement.width, image.placement.height).cast(),
&ProtoGraphics {
id: kludgine.id,
device,
queue,
binding_layout: &kludgine.binding_layout,
linear_sampler: &kludgine.linear_sampler,
nearest_sampler: &kludgine.nearest_sampler,
uniforms: &kludgine.uniforms.wgpu,
},
),
false,
))
}
SwashContent::SubpixelMask => None,
})
};
let blit = if let Some(cached) = cached {
glyphs
.entry(physical.cache_key)
.or_insert_with(|| cached.clone());
GlyphBlit::Visible {
blit: TextureBlit::new(
cached.texture.region,
Rect::new(
(Point::new(physical.x, physical.y)).cast::<Px>()
+ Point::new(
image.placement.left,
metrics.line_height.cast::<i32>() - image.placement.top,
),
Size::new(
i32::try_from(image.placement.width)
.expect("width out of range of i32"),
i32::try_from(image.placement.height)
.expect("height out of range of i32"),
)
.cast(),
),
color,
),
glyph: cached.clone(),
}
} else {
GlyphBlit::Invisible {
location: Point::new(physical.x, physical.y).cast::<Px>(),
width: glyph.w.cast(),
}
};
map(
blit,
glyph,
(run.line_top / metrics.line_height).round().cast::<usize>(),
relative_to.y,
Px::from(run.line_w.ceil()),
kludgine,
);
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum GlyphBlit {
Invisible {
location: Point<Px>,
width: Px,
},
Visible {
blit: TextureBlit<Px>,
glyph: CachedGlyphHandle,
},
}
impl GlyphBlit {
pub fn top_left(&self) -> Point<Px> {
match self {
GlyphBlit::Invisible { location, .. } => *location,
GlyphBlit::Visible { blit, .. } => blit.top_left().location,
}
}
pub fn bottom_right(&self, bottom: Px) -> Point<Px> {
match self {
GlyphBlit::Invisible { location, width } => Point::new(location.x + *width, bottom),
GlyphBlit::Visible { blit, .. } => blit.bottom_right().location,
}
}
}
impl CanRenderTo for GlyphBlit {
fn can_render_to(&self, kludgine: &Kludgine) -> bool {
match self {
GlyphBlit::Invisible { .. } => true,
GlyphBlit::Visible { glyph, .. } => glyph.texture.can_render_to(kludgine),
}
}
}
pub(crate) fn measure_text<Unit, const COLLECT_GLYPHS: bool>(
buffer: Option<&cosmic_text::Buffer>,
color: Color,
kludgine: &mut Kludgine,
device: &wgpu::Device,
queue: &wgpu::Queue,
glyphs: &mut HashMap<cosmic_text::CacheKey, CachedGlyphHandle, DefaultHasher>,
) -> MeasuredText<Unit>
where
Unit: figures::ScreenUnit,
{
let line_height = Unit::from_lp(kludgine.text.line_height, kludgine.effective_scale);
let mut min = Point::new(Px::MAX, Px::MAX);
let mut first_line_max_y = Px::MIN;
let mut last_baseline = Px::MIN;
let mut max = Point::new(Px::MIN, Px::MIN);
let mut measured_glyphs = Vec::new();
map_each_glyph(
buffer,
color,
TextOrigin::TopLeft,
kludgine,
device,
queue,
glyphs,
|blit, glyph, line_index, baseline, line_width, _kludgine| {
last_baseline = last_baseline.max(baseline);
min = min.min(blit.top_left());
max.x = max.x.max(line_width);
max.y = max.y.max(blit.bottom_right(baseline).y);
if line_index == 0 {
first_line_max_y = first_line_max_y.max(blit.bottom_right(baseline).y);
}
if COLLECT_GLYPHS {
measured_glyphs.push(MeasuredGlyph {
blit,
info: GlyphInfo::new(glyph, line_index, line_width),
});
}
},
);
if min == Point::new(Px::MAX, Px::MAX) {
MeasuredText {
ascent: Unit::default(),
descent: Unit::default(),
left: Unit::default(),
line_height,
size: Size::new(Unit::default(), line_height),
glyphs: Vec::new(),
}
} else {
if first_line_max_y == Px::MIN {
first_line_max_y = line_height.into_px(kludgine.effective_scale);
}
MeasuredText {
ascent: line_height - Unit::from_px(min.y, kludgine.effective_scale),
descent: line_height - Unit::from_px(first_line_max_y, kludgine.effective_scale),
left: Unit::from_px(min.x, kludgine.effective_scale),
size: Size {
width: Unit::from_px(max.x, kludgine.effective_scale),
height: Unit::from_px(max.y.max(last_baseline), kludgine.effective_scale)
.max(line_height),
},
line_height,
glyphs: measured_glyphs,
}
}
}
pub struct PreparedText {
graphic: PreparedGraphic<Px>,
_glyphs: HashMap<cosmic_text::CacheKey, CachedGlyphHandle, DefaultHasher>,
}
impl fmt::Debug for PreparedText {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.graphic.fmt(f)
}
}
impl std::ops::Deref for PreparedText {
type Target = PreparedGraphic<Px>;
fn deref(&self) -> &Self::Target {
&self.graphic
}
}
impl std::ops::DerefMut for PreparedText {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.graphic
}
}
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum TextOrigin<Unit> {
#[default]
TopLeft,
Center,
FirstBaseline,
Custom(Point<Unit>),
}
impl<Unit> ScreenScale for TextOrigin<Unit>
where
Unit: ScreenScale<Px = Px, Lp = Lp, UPx = UPx>,
{
type Lp = TextOrigin<Unit::Lp>;
type Px = TextOrigin<Unit::Px>;
type UPx = TextOrigin<Unit::UPx>;
fn into_px(self, scale: Fraction) -> Self::Px {
match self {
TextOrigin::TopLeft => TextOrigin::TopLeft,
TextOrigin::Center => TextOrigin::Center,
TextOrigin::FirstBaseline => TextOrigin::FirstBaseline,
TextOrigin::Custom(pt) => TextOrigin::Custom(pt.into_px(scale)),
}
}
fn from_px(px: Self::Px, scale: Fraction) -> Self {
match px {
TextOrigin::TopLeft => TextOrigin::TopLeft,
TextOrigin::Center => TextOrigin::Center,
TextOrigin::FirstBaseline => TextOrigin::FirstBaseline,
TextOrigin::Custom(pt) => TextOrigin::Custom(Point::from_px(pt, scale)),
}
}
fn into_lp(self, scale: Fraction) -> Self::Lp {
match self {
TextOrigin::TopLeft => TextOrigin::TopLeft,
TextOrigin::Center => TextOrigin::Center,
TextOrigin::FirstBaseline => TextOrigin::FirstBaseline,
TextOrigin::Custom(pt) => TextOrigin::Custom(pt.into_lp(scale)),
}
}
fn from_lp(dips: Self::Lp, scale: Fraction) -> Self {
match dips {
TextOrigin::TopLeft => TextOrigin::TopLeft,
TextOrigin::Center => TextOrigin::Center,
TextOrigin::FirstBaseline => TextOrigin::FirstBaseline,
TextOrigin::Custom(pt) => TextOrigin::Custom(Point::from_lp(pt, scale)),
}
}
fn into_upx(self, scale: Fraction) -> Self::UPx {
match self {
TextOrigin::TopLeft => TextOrigin::TopLeft,
TextOrigin::Center => TextOrigin::Center,
TextOrigin::FirstBaseline => TextOrigin::FirstBaseline,
TextOrigin::Custom(pt) => TextOrigin::Custom(pt.into_upx(scale)),
}
}
fn from_upx(px: Self::UPx, scale: Fraction) -> Self {
match px {
TextOrigin::TopLeft => TextOrigin::TopLeft,
TextOrigin::Center => TextOrigin::Center,
TextOrigin::FirstBaseline => TextOrigin::FirstBaseline,
TextOrigin::Custom(px) => TextOrigin::Custom(Point::from_upx(px, scale)),
}
}
}
#[derive(Debug, Clone)]
pub struct MeasuredText<Unit> {
pub ascent: Unit,
pub descent: Unit,
pub left: Unit,
pub line_height: Unit,
pub size: Size<Unit>,
pub glyphs: Vec<MeasuredGlyph>,
}
impl<Unit> CanRenderTo for MeasuredText<Unit> {
fn can_render_to(&self, kludgine: &Kludgine) -> bool {
self.glyphs
.first()
.map_or(true, |glyph| glyph.can_render_to(kludgine))
}
}
impl<Unit> DrawableSource for MeasuredText<Unit> {}
#[derive(Clone)]
pub struct MeasuredGlyph {
pub(crate) blit: GlyphBlit,
pub info: GlyphInfo,
}
impl Debug for MeasuredGlyph {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MeasuredGlyph")
.field("blit", &self.blit)
.field("info", &self.info)
.finish_non_exhaustive()
}
}
impl MeasuredGlyph {
#[must_use]
pub fn rect(&self) -> Rect<Px> {
let top_left = self.blit.top_left();
Rect::from_extents(top_left, self.blit.bottom_right(top_left.y))
}
#[must_use]
pub const fn visible(&self) -> bool {
matches!(self.blit, GlyphBlit::Visible { .. })
}
}
impl CanRenderTo for MeasuredGlyph {
fn can_render_to(&self, kludgine: &Kludgine) -> bool {
self.blit.can_render_to(kludgine)
}
}
#[derive(Debug, Clone, Copy)]
pub struct GlyphInfo {
pub start: usize,
pub end: usize,
pub line: usize,
pub line_width: Px,
pub level: unicode_bidi::Level,
pub metadata: usize,
}
impl GlyphInfo {
fn new(glyph: &LayoutGlyph, line: usize, line_width: Px) -> Self {
Self {
start: glyph.start,
end: glyph.end,
line,
line_width,
metadata: glyph.metadata,
level: glyph.level,
}
}
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct Text<'a, Unit> {
pub text: &'a str,
pub color: Color,
pub origin: TextOrigin<Unit>,
pub wrap_at: Option<Unit>,
}
impl<'a, Unit> Text<'a, Unit> {
#[must_use]
pub const fn new(text: &'a str, color: Color) -> Self {
Self {
text,
color,
origin: TextOrigin::TopLeft,
wrap_at: None,
}
}
#[must_use]
pub fn origin(mut self, origin: TextOrigin<Unit>) -> Self {
self.origin = origin;
self
}
#[must_use]
pub fn wrap_at(mut self, width: Unit) -> Self {
self.wrap_at = Some(width);
self
}
}
impl<'a, Unit> From<&'a str> for Text<'a, Unit> {
fn from(value: &'a str) -> Self {
Self::new(value, Color::WHITE)
}
}
impl<'a, Unit> From<&'a String> for Text<'a, Unit> {
fn from(value: &'a String) -> Self {
Self::new(value, Color::WHITE)
}
}
impl<'a, Unit> DrawableSource for Text<'a, Unit> {}