-
-
Notifications
You must be signed in to change notification settings - Fork 376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(text): support conversion from Display to Span, Line and Text #1167
feat(text): support conversion from Display to Span, Line and Text #1167
Conversation
Now you can create `Line` and `Text` from numbers like so: ```rust let line = Line::from(42); let text = Text::from(666); ```
I also looked into using something more generic (like |
This doesn't seem like something that would be expected idiomatically in rust.
I'd expect to generally use Perhaps this could be implemented as traits: |
What about creating a Span from every Display implementation? |
I like the idea - I'd be concerned about whether this is too magical for newer users. Any ideas about whether there could be other downsides of this? |
Yeah, I see your point & makes sense. I'm happy to take the
Can you elaborate this? I didn't quite get what this would look like. |
perhaps either: impl<T:Display> From<T> for Span { ... }
// or
impl <T: Display> ToSpan for T { ... } I suspect the first approach might clash with other existing / future generic implementations, so it's something to be careful with. It's a reasonable idea that's worth exploring, but I'd want to see how it pans out in real use cases to evaluate whether it really works well. |
Maybe a more explicit |
The ToSpan trait idea is more in line with ToString in std. https://doc.rust-lang.org/std/string/struct.String.html#impl-ToString-for-T impl<T: fmt::Display + ?Sized> ToString for T { ... } So (copying wording etc. from std too): /// A trait for converting a value to a [`Span`].
///
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
/// such, `ToSpan` shouln't be implemented directly: [`Display`] should be implemented instead, and
/// you get the `ToSpan` implementation for free.
///
/// [`Display`]: std::fmt::Display
pub trait ToSpan {
fn to_span(&self) -> Span<'_>;
}
/// # Panics
///
/// In this implementation, the `to_span` method panics if the `Display` implementation returns an
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
/// returns an error itself.
impl<T: fmt::Display> ToSpan for T {
fn to_span(&self) -> Span<'_> {
Span::raw(self.to_string())
}
} #[test]
fn test_num() {
assert_eq!(42.to_span(), Span::raw("42"));
} ... and ToLine / ToText follow pretty nicely. |
That looks good! Implemented in bdb2096 However, this is not exactly what I wanted to see: -let text = Text::from(42);
+let text = Text::from(42.to_span()); To take the value directly I'm guessing we need |
What about adding let text = 42.to_text(); |
I like the
I'd prefer upstreaming the macros from span!(42)
// or
line![42]
// or
text![42] |
They would create a Line and a Text, not arrays. I think these extra traits seem to add some consistency. In particular, if you know that the Display implementation of something is multi-line, then you should expect to be able to convert that to text, not to a span / line.
This is not necessarily an alternative - wouldn't it make sense to do both? |
The only difference of |
As a direct conversion perhaps not, but as an intermediate value where a line is composed, or just as a more obviously explicit call where a line is expected let mut line = 42.to_line();
line.push_span("%".red());
let mut text = Text::from("asdf");
text.push_line(42.to_line()); I definitely agree that ToSpan and ToText have strong reasons to belong. ToLine is a weaker reason, but still useful (and for consistency this makes sense). There's no obvious downside to this. Handling newlines should be the same as for Line - they get swallowed afaik (noting that there might be some ways to get newline characters in there). If there is no to_line then the latter might be written as: text.push_line(42.to_span());
// which does something completely different (but not obviously so) to:
text.push_span(42.to_span()); |
When the length is already known it's also more performant to write it differently: let line = Line::from([42.to_span(), "%".red()];
As long as the concept of Span, Line, and Text are clear, this isn't confusing. When the concept isn't clear, |
Ed, you're straw-manning the argument. The examples are intentionally simplified. Try strong-manning instead and see if you're able to find some examples that do make sense for ToLine to exist. Particularly try taking on the new rust user perspective. |
My mental model is that we have
Having something that can convert multiple lines separated by |
One way to approach deciding these kinds of API questions is to go find larger examples, in code that is already out there today, that would be improved if it could use the new API. It is difficult to assess whether small examples are good ideas in isolation, and easier if, say, a 10-20 line code sample is improved in some way. |
Thinking about this more, I think there's a bit of a problem with using the Display for any of this - what if you'd want to put styling info directly on the type you're dealing with instead of external to the type. Let's take an example like: struct MarkdownHeading {
content: String,
...
}
impl fmt::Display for MarkdownHeading {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", content)
}
}
// but what if I want this to be always styled
impl ToLine for MarkdownHeading {
fn to_line(&self) -> Line {
content.blue().bold().into()
}
} The same could be said for Span and Text easily - if we implement ToXXX for Display, then we prevent the ability for types to implement their own conversion to Span/Line/Text through these methods. Perhaps these traits might be better named ToRawSpan, ToRawLine, ToRawText?
Ah, I see. Mine is that these are all just Where I really do like this idea is that it provides a more natural way to compose text/line oriented things (like the markdown example, or writing pretty printed JSON). If you have a container of things which implement |
It is probably better to talk about doing this for the But I agree with you about the bit of a problem. I usually think it is a good practice to define a trait/protocol/interface next to its consumer. That is the case with The benefit of this approach is that the two can be designed together. For example, It might be convenient to take advantage of a type's ...and if talking about supporting the
Analogous to |
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #1167 +/- ##
=====================================
Coverage 94.4% 94.4%
=====================================
Files 62 62
Lines 14941 14959 +18
=====================================
+ Hits 14110 14128 +18
Misses 831 831 ☔ View full report in Codecov by Sentry. |
I personally like the idea of having I'm happy to rename it to
Can't we export the trait and let the users implement it for their own types? Also, we should think about how common that use-case is too. |
See https://github.com/rust-lang/rfcs/blob/master/text/0565-show-string-guidelines.md#user-facing-fmtdisplay, which talks about how it is expected that application code will use a different type (most likely a newtype) for each variation of |
I think what you're saying is if the widget implements display, and that's not what we want to be rendered, then it's easy enough to do @orhun we can export the trait, but my point was that you can't implement it twice on the one type, and implementing it as a blanket implementation for every type that implements display would be the first implementation in some cases. I'd guess this is probably rare enough that it shouldn't matter too much though (and the workaround above is fine). |
Addressed the review comments and rebased on main. Now the only issue is:
Should we come up places to use this new mechanism somewhere in the code or just slap |
Nothing It took me a while to figure this out, but it turns out that the new traits were not actually pub outside Ratatui. Try the patch below (which causes new warnings about no doc comment for the trait methods): diff --git a/src/text.rs b/src/text.rs
index 6262ca6..92aad3a 100644
--- a/src/text.rs
+++ b/src/text.rs
@@ -49,13 +49,16 @@ pub use grapheme::StyledGrapheme;
mod line;
pub use line::Line;
+pub use line::ToLine;
mod masked;
pub use masked::Masked;
mod span;
pub use span::Span;
+pub use span::ToSpan;
#[allow(clippy::module_inception)]
mod text;
pub use text::Text;
+pub use text::ToText; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check clippy
Thanks for the pointers guys, it should be good to go now! |
Line
and Text
from usize
…tui#1167) Now you can create `Line` and `Text` from numbers like so: ```rust let line = Line::from(42); let text = Text::from(666); ``` (I was doing little testing for my TUI app and saw that this isn't supported - then I was like WHA and decided to make it happen ™️)
Now you can create
Line
andText
from numbers like so:(I was doing little testing for my TUI app and saw that this isn't supported - then I was like WHA and decided to make it happen ™️)