645 lines
23 KiB
Rust
645 lines
23 KiB
Rust
use std::borrow::Cow;
|
||
|
||
use cosmic_text::{self, Align};
|
||
|
||
use cosmic_text::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, Style, Weight};
|
||
#[cfg(feature = "databake")]
|
||
use databake::Bake;
|
||
use emath::{Pos2, Rect, Vec2};
|
||
use jotdown::{Container, Event, ListKind, OrderedListNumbering, OrderedListStyle};
|
||
use nominals::{LetterLower, LetterUpper, Nominal, RomanLower, RomanUpper};
|
||
|
||
#[cfg(any(feature = "serde", feature = "databake"))]
|
||
pub mod serde_suck;
|
||
#[cfg(feature = "serde")]
|
||
use serde::{Deserialize, Serialize};
|
||
#[cfg(any(feature = "serde", feature = "databake"))]
|
||
pub use serde_suck::*;
|
||
|
||
use rangemap::RangeMap;
|
||
|
||
pub struct JotdownBufferIter<'a, T: Iterator<Item = Event<'a>>> {
|
||
pub djot: T,
|
||
pub indent: Vec<Indent>,
|
||
}
|
||
|
||
struct JotdownIntoBuffer<'a, 'b, T: Iterator<Item = Event<'a>>> {
|
||
pub djot: &'b mut T,
|
||
pub attrs: Attrs<'static>,
|
||
pub metrics: Metrics,
|
||
pub indent: &'b mut Vec<Indent>,
|
||
pub image_url: Option<Cow<'a, str>>,
|
||
pub link_start: usize,
|
||
pub urls: Vec<(std::ops::Range<usize>, Cow<'a, str>)>,
|
||
pub location: usize,
|
||
pub added: bool,
|
||
pub top_level_container: Option<Container<'a>>,
|
||
}
|
||
|
||
#[derive(Default, Clone, Copy, Debug)]
|
||
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
|
||
pub struct Indent {
|
||
#[cfg_attr(feature = "serde", serde(with = "ListKindOption"))]
|
||
pub modifier: Option<ListKind>,
|
||
pub indent: f32,
|
||
}
|
||
|
||
#[cfg(feature = "databake")]
|
||
impl databake::Bake for Indent {
|
||
fn bake(&self, ctx: &databake::CrateEnv) -> databake::TokenStream {
|
||
let indent = self.indent;
|
||
let modifier = self.modifier.bake(ctx);
|
||
databake::quote! {
|
||
cosmic_jotdown::Indent {
|
||
indent: #indent,
|
||
modifier: #modifier,
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
pub const INDENT_AMOUNT: f32 = 16.0;
|
||
|
||
impl<'a, 'b, T: Iterator<Item = Event<'a>>> Iterator for JotdownIntoBuffer<'a, 'b, T> {
|
||
type Item = (&'a str, Attrs<'static>);
|
||
|
||
fn next(&mut self) -> Option<Self::Item> {
|
||
while let Some(event) = self.djot.next() {
|
||
match event {
|
||
Event::LeftSingleQuote => {
|
||
self.added = true;
|
||
self.location += "‘".len();
|
||
return Some(("‘", self.attrs.clone()));
|
||
}
|
||
Event::LeftDoubleQuote => {
|
||
self.added = true;
|
||
self.location += "“".len();
|
||
return Some(("“", self.attrs.clone()));
|
||
}
|
||
Event::RightSingleQuote => {
|
||
self.added = true;
|
||
self.location += "’".len();
|
||
return Some(("’", self.attrs.clone()));
|
||
}
|
||
Event::RightDoubleQuote => {
|
||
self.added = true;
|
||
self.location += "”".len();
|
||
return Some(("”", self.attrs.clone()));
|
||
}
|
||
Event::Ellipsis => {
|
||
self.added = true;
|
||
self.location += "…".len();
|
||
return Some(("…", self.attrs.clone()));
|
||
}
|
||
Event::EmDash => {
|
||
self.added = true;
|
||
self.location += "—".len();
|
||
return Some(("—", self.attrs.clone()));
|
||
}
|
||
Event::EnDash => {
|
||
self.added = true;
|
||
self.location += "–".len();
|
||
return Some(("–", self.attrs.clone()));
|
||
}
|
||
Event::Softbreak | Event::NonBreakingSpace => {
|
||
self.added = true;
|
||
self.location += " ".len();
|
||
return Some((" ", self.attrs.clone()));
|
||
}
|
||
Event::Str(Cow::Borrowed(s)) | Event::Symbol(Cow::Borrowed(s)) => {
|
||
self.added = true;
|
||
self.location += s.len();
|
||
return Some((s, self.attrs.clone()));
|
||
}
|
||
Event::Start(container, _) => match container {
|
||
Container::Heading { level, .. } => {
|
||
let l = match level {
|
||
1 => 2.0,
|
||
2 => 1.5,
|
||
3 => 1.17,
|
||
4 => 1.0,
|
||
5 => 0.83,
|
||
_ => 0.67,
|
||
};
|
||
self.metrics = Metrics::new(l, l * 1.1);
|
||
self.top_level_container.get_or_insert(container);
|
||
}
|
||
Container::Emphasis => self.attrs = self.attrs.style(Style::Italic),
|
||
Container::Strong => self.attrs = self.attrs.weight(Weight::BOLD),
|
||
Container::Verbatim => self.attrs = self.attrs.family(Family::Monospace),
|
||
Container::ListItem { .. } | Container::Paragraph => {
|
||
self.top_level_container.get_or_insert(container);
|
||
}
|
||
Container::List { kind, .. } => self.indent.push(Indent {
|
||
indent: self.indent.last().copied().unwrap_or_default().indent
|
||
+ INDENT_AMOUNT,
|
||
modifier: Some(kind),
|
||
}),
|
||
Container::Image(ref url, _) => {
|
||
self.added = true;
|
||
self.image_url = Some(url.clone());
|
||
self.top_level_container.get_or_insert(container);
|
||
}
|
||
Container::Link(_, _) => {
|
||
self.link_start = self.location + 1;
|
||
self.attrs = self.attrs.color(Color::rgb(96, 198, 233));
|
||
}
|
||
_ => {}
|
||
},
|
||
Event::End(container) => match container {
|
||
Container::Emphasis => self.attrs = self.attrs.style(Style::Normal),
|
||
Container::Strong => self.attrs = self.attrs.weight(Weight::NORMAL),
|
||
Container::Verbatim => self.attrs = self.attrs.family(Family::SansSerif),
|
||
Container::List { .. } => {
|
||
self.indent.pop();
|
||
}
|
||
Container::Heading { .. } | Container::Paragraph | Container::Image(_, _) => {
|
||
if self.added {
|
||
return None;
|
||
}
|
||
}
|
||
Container::Link(Cow::Borrowed(url), _) => {
|
||
self.urls
|
||
.push((self.link_start..self.location, Cow::Borrowed(url)));
|
||
self.attrs = self.attrs.color(Color::rgb(255, 255, 255));
|
||
}
|
||
_ => {}
|
||
},
|
||
Event::Hardbreak | Event::ThematicBreak(_) => {}
|
||
Event::Blankline | Event::Escape | Event::FootnoteReference(_) => {}
|
||
Event::Str(Cow::Owned(_)) | Event::Symbol(Cow::Owned(_)) => panic!(),
|
||
}
|
||
}
|
||
|
||
return None;
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
|
||
pub struct RichText<'a>(
|
||
#[cfg_attr(feature = "serde", serde(borrow))] pub Cow<'a, str>,
|
||
#[cfg_attr(feature = "serde", serde(with = "AttrsSerde"))] pub Attrs<'a>,
|
||
);
|
||
|
||
#[cfg(feature = "databake")]
|
||
impl<'a> Bake for RichText<'a> {
|
||
fn bake(&self, ctx: &databake::CrateEnv) -> databake::TokenStream {
|
||
let field0 = self.0.bake(ctx);
|
||
let field1: AttrsSerde = self.1.into();
|
||
let field1 = field1.bake(ctx);
|
||
databake::quote! {
|
||
cosmic_jotdown::RichText(#field0, #field1)
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
|
||
#[cfg_attr(feature = "databake", derive(databake::Bake))]
|
||
#[cfg_attr(feature = "databake", databake(path = cosmic_jotdown))]
|
||
pub struct JotdownItem<'a> {
|
||
pub indent: Indent,
|
||
#[cfg_attr(feature = "serde", serde(borrow))]
|
||
pub buffer: Cow<'a, [RichText<'a>]>,
|
||
pub metrics: (f32, f32),
|
||
pub image_url: Option<Cow<'a, str>>,
|
||
pub margin: f32,
|
||
pub url_map: Option<Cow<'a, [(usize, usize, Cow<'a, str>)]>>,
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
pub struct ResolvedJotdownItem<'a> {
|
||
pub indent: Indent,
|
||
pub buffer: Buffer,
|
||
pub metrics: Metrics,
|
||
pub relative_bounds: Rect,
|
||
pub url_map: Option<RangeMap<usize, Cow<'a, str>>>,
|
||
pub image_urls: Option<(String, String)>,
|
||
}
|
||
|
||
pub fn resolve_paragraphs<'a>(
|
||
paragaphs: &[JotdownItem<'a>],
|
||
viewbox: Vec2,
|
||
font_system: &mut FontSystem,
|
||
metrics: Metrics,
|
||
align: Option<Align>,
|
||
factor: f32,
|
||
base_uri: &'static str,
|
||
) -> (Vec2, Vec<ResolvedJotdownItem<'a>>) {
|
||
let mut size = Vec2::new(0.0, 0.0);
|
||
// Cannot be f32::MAX
|
||
let mut last_margin = 99999.0;
|
||
let mut last_image_size: Option<Vec2> = None;
|
||
let mut first = true;
|
||
let mut in_list = false;
|
||
let mut list_number = 0;
|
||
let mut paragraphs = paragaphs
|
||
.iter()
|
||
.flat_map(|jbuffer| {
|
||
let mut buffer =
|
||
jbuffer
|
||
.clone()
|
||
.resolve(font_system, viewbox.x, metrics, align, factor);
|
||
let margin = jbuffer.margin * buffer.metrics.line_height;
|
||
|
||
let margin_top = (margin - last_margin).max(0.0);
|
||
buffer.relative_bounds = buffer
|
||
.relative_bounds
|
||
.translate(Vec2::new(buffer.indent.indent, size.y + margin_top));
|
||
|
||
let buffer_size = buffer.relative_bounds.size();
|
||
let buffer_indent = buffer.indent.indent;
|
||
let result_buffers = if let Some(url) = &jbuffer.image_url {
|
||
let url = url.split_once('#').unwrap();
|
||
let image_size = url.1.split_once('x').unwrap();
|
||
let image_size =
|
||
Vec2::new(image_size.0.parse().unwrap(), image_size.1.parse().unwrap());
|
||
let image = format!("{}/{}", base_uri, url.0);
|
||
let split = url.0.rsplit_once('.').unwrap();
|
||
let hi_image = format!("{}/{}_hi.{}", base_uri, split.0, split.1);
|
||
buffer.image_urls = Some((image, hi_image));
|
||
log::info!("{:?}", buffer.image_urls);
|
||
buffer.relative_bounds =
|
||
Rect::from_min_size(Pos2::new(buffer.indent.indent, size.y), image_size);
|
||
const IMAGE_PADDING: f32 = 8.0;
|
||
if let Some(last_size) = last_image_size.as_mut() {
|
||
let ls = *last_size;
|
||
last_size.x += image_size.x + IMAGE_PADDING;
|
||
|
||
if last_size.x > viewbox.x {
|
||
size.y += last_size.y;
|
||
last_size.x = image_size.x + IMAGE_PADDING;
|
||
last_size.y = image_size.y + margin;
|
||
buffer.relative_bounds = Rect::from_min_size(
|
||
Pos2::new(buffer.indent.indent, size.y),
|
||
image_size,
|
||
);
|
||
} else {
|
||
last_size.y = last_size.y.max(image_size.y + margin);
|
||
buffer.relative_bounds =
|
||
buffer.relative_bounds.translate(Vec2::new(ls.x, 0.0));
|
||
}
|
||
} else {
|
||
if image_size.x <= viewbox.x {
|
||
last_image_size = Some(image_size + Vec2::new(IMAGE_PADDING, margin));
|
||
}
|
||
}
|
||
|
||
if image_size.x > viewbox.x {
|
||
let max_size = Vec2::new(viewbox.x, image_size.y);
|
||
let new_size = scale_to_fit(image_size, max_size, true);
|
||
buffer.relative_bounds =
|
||
Rect::from_min_size(Pos2::new(buffer.indent.indent, size.y), new_size);
|
||
if let Some(last_image_size) = &mut last_image_size {
|
||
last_image_size.y -= image_size.y - new_size.y;
|
||
} else {
|
||
size.y += new_size.y + margin
|
||
}
|
||
}
|
||
|
||
[Some(buffer), None]
|
||
} else if let Some(mut list_kind) = buffer.indent.modifier {
|
||
if let Some(image_size) = last_image_size {
|
||
size.y += image_size.y;
|
||
buffer.relative_bounds = buffer
|
||
.relative_bounds
|
||
.translate(Vec2::new(0.0, image_size.y));
|
||
size.x = size.x.max(image_size.x);
|
||
last_image_size = None;
|
||
}
|
||
|
||
if let ListKind::Ordered { start, .. } = &mut list_kind {
|
||
if !in_list {
|
||
list_number = *start;
|
||
} else {
|
||
list_number += 1;
|
||
}
|
||
|
||
*start = list_number;
|
||
}
|
||
|
||
in_list = true;
|
||
|
||
let mut list_buffer = Buffer::new(font_system, buffer.metrics);
|
||
list_buffer.set_text(
|
||
font_system,
|
||
make_list_number(list_kind).as_ref(),
|
||
Attrs::new().family(Family::SansSerif),
|
||
Shaping::Advanced,
|
||
);
|
||
|
||
list_buffer.set_wrap(font_system, cosmic_text::Wrap::WordOrGlyph);
|
||
list_buffer.set_size(font_system, f32::MAX, f32::MAX);
|
||
|
||
list_buffer.shape_until_scroll(font_system, false);
|
||
|
||
let list_buffer_metrics = buffer.metrics;
|
||
let indent = (buffer_indent) - (INDENT_AMOUNT * factor);
|
||
let res = [
|
||
Some(buffer),
|
||
Some(ResolvedJotdownItem {
|
||
indent: Indent {
|
||
modifier: None,
|
||
indent,
|
||
},
|
||
relative_bounds: measure_buffer(
|
||
&list_buffer,
|
||
Vec2::new(f32::MAX, f32::MAX),
|
||
)
|
||
.translate(Vec2::new(indent, size.y + margin_top)),
|
||
buffer: list_buffer,
|
||
metrics: list_buffer_metrics,
|
||
url_map: None,
|
||
image_urls: None,
|
||
}),
|
||
];
|
||
size.y += buffer_size.y + (margin_top + margin);
|
||
size.x = size.x.max(buffer_size.x + buffer_indent);
|
||
res
|
||
} else {
|
||
if let Some(image_size) = last_image_size {
|
||
size.y += image_size.y;
|
||
buffer.relative_bounds = buffer
|
||
.relative_bounds
|
||
.translate(Vec2::new(0.0, image_size.y));
|
||
size.x = size.x.max(image_size.x);
|
||
last_image_size = None;
|
||
}
|
||
|
||
in_list = false;
|
||
size.y += buffer_size.y + (margin_top + margin);
|
||
size.x = size.x.max(buffer_size.x + buffer_indent);
|
||
|
||
[Some(buffer), None]
|
||
};
|
||
|
||
last_margin = margin;
|
||
first = false;
|
||
|
||
result_buffers
|
||
})
|
||
.filter_map(|p| p)
|
||
.collect::<Vec<_>>();
|
||
|
||
size.y -= last_margin;
|
||
|
||
if let Some(image_size) = last_image_size {
|
||
size.y += image_size.y;
|
||
}
|
||
|
||
if align.is_some() {
|
||
paragraphs.iter_mut().for_each(|paragraph| {
|
||
if paragraph.image_urls.is_none() {
|
||
paragraph
|
||
.relative_bounds
|
||
.set_width(size.x - paragraph.indent.indent);
|
||
paragraph
|
||
.buffer
|
||
.set_size(font_system, size.x - paragraph.indent.indent, f32::MAX);
|
||
paragraph.buffer.shape_until_scroll(font_system, false);
|
||
}
|
||
});
|
||
}
|
||
|
||
(size, paragraphs)
|
||
}
|
||
|
||
impl<'a> JotdownItem<'a> {
|
||
pub fn resolve(
|
||
mut self,
|
||
font_system: &mut FontSystem,
|
||
width: f32,
|
||
metrics: Metrics,
|
||
align: Option<Align>,
|
||
factor: f32,
|
||
) -> ResolvedJotdownItem<'a> {
|
||
self.indent.indent *= factor;
|
||
|
||
let buffer = self.make_buffer(font_system, width, metrics, align);
|
||
|
||
ResolvedJotdownItem {
|
||
indent: self.indent,
|
||
relative_bounds: measure_buffer(&buffer, Vec2::new(width, f32::MAX)),
|
||
buffer,
|
||
metrics: Metrics::new(
|
||
self.metrics.0 * metrics.font_size,
|
||
self.metrics.1 * metrics.line_height,
|
||
),
|
||
url_map: {
|
||
self.url_map.map(|self_url_map| {
|
||
let mut url_map = RangeMap::new();
|
||
url_map.extend(self_url_map.iter().map(|m| (m.0..m.1, m.2.clone())));
|
||
url_map
|
||
})
|
||
},
|
||
image_urls: None,
|
||
}
|
||
}
|
||
|
||
pub fn new_default(text: Vec<RichText<'a>>) -> Self {
|
||
Self {
|
||
indent: Indent {
|
||
modifier: None,
|
||
indent: 0.0,
|
||
},
|
||
buffer: Cow::Owned(text),
|
||
metrics: (1.0, 1.1),
|
||
url_map: None,
|
||
margin: 0.0,
|
||
image_url: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl<'a> JotdownItem<'a> {
|
||
pub fn make_buffer(
|
||
&self,
|
||
font_system: &mut FontSystem,
|
||
width: f32,
|
||
metrics: Metrics,
|
||
align: Option<Align>,
|
||
) -> Buffer {
|
||
let mut buffer = Buffer::new(
|
||
font_system,
|
||
Metrics::new(
|
||
metrics.font_size * self.metrics.0,
|
||
metrics.line_height * self.metrics.1,
|
||
),
|
||
);
|
||
buffer.set_rich_text(
|
||
font_system,
|
||
self.buffer.iter().map(|r| (r.0.as_ref(), r.1)),
|
||
Attrs::new().family(Family::SansSerif),
|
||
Shaping::Advanced,
|
||
);
|
||
|
||
buffer.set_wrap(font_system, cosmic_text::Wrap::WordOrGlyph);
|
||
buffer.set_size(font_system, width - self.indent.indent, f32::MAX);
|
||
|
||
for line in &mut buffer.lines {
|
||
line.set_align(align);
|
||
}
|
||
|
||
buffer.shape_until_scroll(font_system, false);
|
||
buffer
|
||
}
|
||
}
|
||
|
||
impl<'a, T: Iterator<Item = Event<'a>>> Iterator for JotdownBufferIter<'a, T> {
|
||
type Item = JotdownItem<'a>;
|
||
|
||
fn next(&mut self) -> Option<Self::Item> {
|
||
let mut jot = JotdownIntoBuffer {
|
||
djot: &mut self.djot,
|
||
attrs: Attrs::new().family(Family::SansSerif),
|
||
indent: &mut self.indent,
|
||
image_url: None,
|
||
metrics: Metrics::new(1.0, 1.1),
|
||
added: false,
|
||
link_start: 0,
|
||
location: 0,
|
||
urls: Vec::new(),
|
||
top_level_container: None,
|
||
};
|
||
|
||
let buffer = (&mut jot)
|
||
.map(|r| RichText(r.0.into(), r.1))
|
||
.collect::<Vec<_>>();
|
||
let image_url = jot.image_url;
|
||
|
||
let urls = jot.urls;
|
||
let top_level_containers = jot.top_level_container;
|
||
let added = jot.added;
|
||
let metrics = jot.metrics;
|
||
let indent = self.indent.last().copied().unwrap_or_default();
|
||
if !added {
|
||
return None;
|
||
} else {
|
||
return Some(JotdownItem {
|
||
indent,
|
||
url_map: if urls.is_empty() {
|
||
None
|
||
} else {
|
||
let mut map = Vec::new();
|
||
map.extend(
|
||
urls.into_iter()
|
||
.map(|(range, url)| (range.start, range.end + 1, url)),
|
||
);
|
||
Some(Cow::Owned(map))
|
||
},
|
||
buffer: Cow::Owned(buffer),
|
||
image_url,
|
||
metrics: (metrics.font_size, metrics.line_height),
|
||
margin: match top_level_containers {
|
||
Some(Container::Heading { level, .. }) => match level {
|
||
1 => 0.34,
|
||
2 => 0.42,
|
||
3 => 0.5,
|
||
4 => 0.65,
|
||
5 => 0.85,
|
||
6 => 1.25,
|
||
_ => unreachable!(),
|
||
},
|
||
Some(Container::ListItem) => 0.5,
|
||
Some(Container::Image(_, _)) => 1.0,
|
||
_ => 1.0,
|
||
},
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn jotdown_into_buffers<'a, T: Iterator<Item = Event<'a>>>(
|
||
djot: T,
|
||
) -> JotdownBufferIter<'a, T> {
|
||
JotdownBufferIter {
|
||
djot,
|
||
indent: Vec::new(),
|
||
}
|
||
}
|
||
|
||
pub fn make_list_number(list_kind: ListKind) -> Cow<'static, str> {
|
||
match list_kind {
|
||
ListKind::Unordered | ListKind::Task => Cow::Borrowed("•"),
|
||
ListKind::Ordered {
|
||
numbering,
|
||
style,
|
||
start: number,
|
||
} => {
|
||
use std::fmt::Write;
|
||
let mut result = String::new();
|
||
|
||
if matches!(style, OrderedListStyle::ParenParen) {
|
||
result.push('(');
|
||
}
|
||
|
||
match numbering {
|
||
OrderedListNumbering::Decimal => {
|
||
result.write_fmt(format_args!("{}", number)).unwrap()
|
||
}
|
||
OrderedListNumbering::AlphaLower => {
|
||
result
|
||
.write_fmt(format_args!("{}", (number - 1).to_nominal(&LetterLower)))
|
||
.unwrap();
|
||
}
|
||
OrderedListNumbering::AlphaUpper => {
|
||
result
|
||
.write_fmt(format_args!("{}", (number - 1).to_nominal(&LetterUpper)))
|
||
.unwrap();
|
||
}
|
||
OrderedListNumbering::RomanLower => {
|
||
result
|
||
.write_fmt(format_args!("{}", number.to_nominal(&RomanLower)))
|
||
.unwrap();
|
||
}
|
||
OrderedListNumbering::RomanUpper => {
|
||
result
|
||
.write_fmt(format_args!("{}", number.to_nominal(&RomanUpper)))
|
||
.unwrap();
|
||
}
|
||
}
|
||
|
||
match style {
|
||
OrderedListStyle::Period => result.push('.'),
|
||
OrderedListStyle::Paren | OrderedListStyle::ParenParen => result.push(')'),
|
||
}
|
||
|
||
Cow::Owned(result)
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn measure_buffer(buffer: &Buffer, vb: Vec2) -> Rect {
|
||
let mut rtl = false;
|
||
let (width, total_lines) =
|
||
buffer
|
||
.layout_runs()
|
||
.fold((0.0, 0usize), |(width, total_lines), run| {
|
||
if run.rtl {
|
||
rtl = true;
|
||
}
|
||
(run.line_w.max(width), total_lines + 1)
|
||
});
|
||
|
||
let (max_width, max_height) = buffer.size();
|
||
|
||
let size = Vec2::new(
|
||
if rtl { vb.x } else { width.min(max_width) },
|
||
(total_lines as f32 * buffer.metrics().line_height).min(max_height),
|
||
);
|
||
|
||
Rect::from_min_size(Pos2::ZERO, size)
|
||
}
|
||
|
||
fn scale_to_fit(image_size: Vec2, available_size: Vec2, maintain_aspect_ratio: bool) -> Vec2 {
|
||
if maintain_aspect_ratio {
|
||
let ratio_x = available_size.x / image_size.x;
|
||
let ratio_y = available_size.y / image_size.y;
|
||
let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
|
||
let ratio = if ratio.is_finite() { ratio } else { 1.0 };
|
||
image_size * ratio
|
||
} else {
|
||
available_size
|
||
}
|
||
}
|