T16 Finance: market charts with kinds, volume, profile, studies, drawings, compare, alerts and sync (T16a–b); quotes, book, tape and DOM (T16c); order entry, blotter, risk, options and payoff (T16d); markets, calendars, portfolio and wallet (T16e); typography::Ellipsis ends lines that gpui's truncate clipped; chart notes stack and ticks label round values
This commit is contained in:
76 files changed
+10311
-173
No files matched your search
+120
-1
@@ -1,7 +1,7 @@
|
||||
use gpui::{
|
||||
App, ElementId, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, ShapedLine,
|
||||
SharedString, StatefulInteractiveElement, Styled, TextRun, Window, canvas, div, prelude::*,
|
||||
relative,
|
||||
relative, transparent_black,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -21,6 +21,23 @@ fn elide(chars: &[char], kept: usize) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/// How wide `text` sets at `size` in the text style around it.
|
||||
pub(crate) fn text_width(text: &str, size: Pixels, window: &Window) -> Pixels {
|
||||
let style = window.text_style();
|
||||
let run = TextRun {
|
||||
len: text.len(),
|
||||
font: style.font(),
|
||||
color: style.color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(SharedString::from(text.to_string()), size, &[run], None)
|
||||
.width
|
||||
}
|
||||
|
||||
fn shape(text: &str, window: &mut Window) -> ShapedLine {
|
||||
let style = window.text_style();
|
||||
let run = TextRun {
|
||||
@@ -62,6 +79,76 @@ fn fit_middle(text: &str, width: Pixels, window: &mut Window) -> String {
|
||||
elide(&chars, low)
|
||||
}
|
||||
|
||||
/// Longest front that fits `width` with an ellipsis after it.
|
||||
fn fit_end(text: &str, width: Pixels, window: &mut Window) -> String {
|
||||
if width_of(text, window) <= width {
|
||||
return text.to_string();
|
||||
}
|
||||
if width_of("…", window) > width {
|
||||
return String::new();
|
||||
}
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let cut = |kept: usize| {
|
||||
let front: String = chars[..kept].iter().collect();
|
||||
let mut out: String = front
|
||||
.trim_end_matches(|ch: char| ch.is_whitespace() || matches!(ch, '.' | ',' | ';' | ':'))
|
||||
.into();
|
||||
out.push('…');
|
||||
out
|
||||
};
|
||||
let (mut low, mut high) = (0, chars.len());
|
||||
while low < high {
|
||||
let mid = (low + high).div_ceil(2);
|
||||
if width_of(&cut(mid), window) <= width {
|
||||
low = mid;
|
||||
} else {
|
||||
high = mid - 1;
|
||||
}
|
||||
}
|
||||
cut(low)
|
||||
}
|
||||
|
||||
/// One line in the text style around it, cut at the end with an ellipsis when its box is too narrow. gpui 0.2.2 keeps a line's first measure, so its own `truncate` clips instead in flex boxes.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Ellipsis {
|
||||
text: SharedString,
|
||||
}
|
||||
|
||||
impl Ellipsis {
|
||||
pub fn new(text: impl Into<SharedString>) -> Self {
|
||||
Self { text: text.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Ellipsis {
|
||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
let text = self.text.clone();
|
||||
div()
|
||||
.relative()
|
||||
.min_w_0()
|
||||
.overflow_hidden()
|
||||
.whitespace_nowrap()
|
||||
.child(div().text_color(transparent_black()).child(self.text))
|
||||
.child(
|
||||
canvas(
|
||||
move |bounds, window, _| {
|
||||
shape(&fit_end(&text, bounds.size.width, window), window)
|
||||
},
|
||||
|bounds, shaped, window, cx| {
|
||||
let line = window.text_style().line_height_in_pixels(window.rem_size());
|
||||
shaped
|
||||
.paint(bounds.origin, line, window, cx)
|
||||
.expect("an ellipsis line paints");
|
||||
},
|
||||
)
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.size_full(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Shortens in the middle, keeping both ends. Paths, hashes.
|
||||
#[derive(IntoElement)]
|
||||
pub struct MiddleEllipsis {
|
||||
@@ -176,3 +263,35 @@ mod tests {
|
||||
assert_eq!(elide(&wide, 4), "路径…rs");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "test-support"))]
|
||||
mod fits {
|
||||
use gpui::{Pixels, TestAppContext};
|
||||
|
||||
use super::{fit_end, width_of};
|
||||
|
||||
#[gpui::test]
|
||||
fn an_ellipsis_keeps_the_longest_front_that_fits(cx: &mut TestAppContext) {
|
||||
let cx = cx.add_empty_window();
|
||||
cx.update(|window, _| {
|
||||
let text = "Good interfaces. Quiet ones";
|
||||
let whole = width_of(text, window);
|
||||
assert_eq!(
|
||||
fit_end(text, whole, window),
|
||||
text,
|
||||
"a line that fits stays whole"
|
||||
);
|
||||
let room = width_of("Good interfaces.…", window);
|
||||
assert_eq!(
|
||||
fit_end(text, room, window),
|
||||
"Good interfaces…",
|
||||
"no stop or space before the ellipsis"
|
||||
);
|
||||
assert_eq!(
|
||||
fit_end(text, Pixels::ZERO, window),
|
||||
"",
|
||||
"no room shows nothing"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ mod text;
|
||||
pub use copy::CopyableText;
|
||||
pub use effects::{GradientText, ShimmerText, Typewriter};
|
||||
pub use emoji::Emoji;
|
||||
pub(crate) use fit::LEADING;
|
||||
pub use fit::{EllipsisTooltip, MiddleEllipsis};
|
||||
pub use fit::{Ellipsis, EllipsisTooltip, MiddleEllipsis};
|
||||
pub(crate) use fit::{LEADING, text_width};
|
||||
pub use format::DurationStyle;
|
||||
pub use formatted::{
|
||||
CurrencyText, DateTimeText, DurationText, FileSizeText, NumberText, PercentText, PluralText,
|
||||
|
||||
Reference in new issue
Block a user