This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2022-03-21
Channels
- # announcements (13)
- # babashka (63)
- # babashka-sci-dev (64)
- # beginners (37)
- # biff (1)
- # calva (10)
- # cider (7)
- # clj-kondo (15)
- # cljsrn (6)
- # clojure (26)
- # clojure-dev (10)
- # clojure-europe (34)
- # clojure-france (9)
- # clojure-nl (2)
- # clojure-norway (36)
- # clojure-uk (5)
- # clojurescript (142)
- # community-development (1)
- # conjure (3)
- # datalevin (5)
- # datalog (2)
- # datomic (5)
- # events (11)
- # fulcro (40)
- # gratitude (9)
- # guix (32)
- # honeysql (10)
- # jobs (2)
- # lsp (32)
- # malli (15)
- # meander (5)
- # membrane (43)
- # missionary (3)
- # nextjournal (9)
- # off-topic (38)
- # pathom (3)
- # polylith (30)
- # portal (78)
- # programming-beginners (4)
- # quil (6)
- # re-frame (20)
- # reagent (21)
- # remote-jobs (2)
- # shadow-cljs (7)
- # tools-deps (6)
- # xtdb (23)
https://github.com/phronmophobic/membrane/blob/e547cdcec33239b3920428a47b9966b93d300be8/src/membrane/java2d.clj#L271 line-height (.getHeight metrics)
gives "SUBMIT2" and "help2" whereas line-height (.getAscent metrics)
would give "SUBMIT" and "help" which look nicer.
I don't have any prior experience with this stuff and it's confusing for me. I https://docs.oracle.com/javase/8/docs/api/java/awt/FontMetrics.html that the font starts drawing at a https://docs.oracle.com/javase/8/docs/api/java/awt/doc-files/FontMetrics-1.gif on the baseline. The rectangle's 0,0 is top left. We have to shift the text but idk by how much. I think I want to treat the ascent plus descent as the height and center that. That ignores the leading which would be relevant for multiple lines of text. My understanding is that the Height is the Ascent+Descent+Leading where the Ascent is the hump of an 'h' to the top (or the top of an 'h' to the top of an 'o') and the Descent is the bottom of a 'p' to the baseline. (or bottom of a 'p' to the bottom of an 'o')
Maybe the rectangle should be Ascent+Descent in height instead of Height. i.e. Height is the result of .getHeight() which is the line height. I'm interested to hear what you think. I started looking into it since my button looked bad. I understand what's going on well enough that I can get something that's not bad.
membrane.ui/label
's API isn't a good fit. It both does too much and too little. For backwards compatibility reasons, I think it's too late to "fix" membrane.ui/label
's behavior, but I definitely want to create a new builtin text element that is more sane and deprecate membrane.ui/label
.
There's some related features that I would want to consider as part of the design of a better text element:
• alignment
• justification
• text direction
• emoji
• styling
• wrapping
• line height
• multi-font
My experience so far is that I don’t like ui/label but I haven’t figured out yet exactly what I don’t like. https://docs.oracle.com/en/java/javase/16/docs/api/java.desktop/java/awt/font/TextLayout.html says it provides justification and text direction. I haven’t tried to use it for anything though so I don’t know in what way it provides those things. Probably not in a way that’s composable with membrane.
word wrap, text wrap
basically, you provide some bounds and have the text wrap to the next line
TextLayout makes a lot of sense for Java2d, but can't be used for any of the other options.
It does look like a good reference though and it wasn't on my radar. Thanks for the pointer!
Did you see this? some font rendering thoughts. https://tonsky.me/blog/font-size/
@U09D96P9B, I have. It's good food for thought. It's still unclear to me the best way to incorporate all the following: • alignment • justification • text direction • emoji • styling • wrapping • line height • multi-font
Something else, why decrement the line height? https://github.com/phronmophobic/membrane/blob/e547cdcec33239b3920428a47b9966b93d300be8/src/membrane/java2d.clj#L251 This makes the lines one pixel closer together on the y axis, right?
I'm not totally sure, but I think the issue is that .getHeight
returns an integer instead of a float which is subtly wrong. I think what happened is I didn't realize why the text layout was wrong and just picked an option that looked reasonable. Later, I found out that there's a separate API that does return the ascent, descent, and leading as float values (see https://github.com/phronmophobic/membrane/blob/e547cdcec33239b3920428a47b9966b93d300be8/src/membrane/java2d.clj#L107), but didn't revisit the text rendering.
good catch!
Created an issue here, https://github.com/phronmophobic/membrane/issues/40
So the bottom line is that I'm not totally sure why line height is decremented. Another guess is that it made text rendering better match skia's, but I don't know off the top of my head.
Sorry I don't have a better answer 😕
Haha, no trouble. I was trying to avoid touching the source so that I could more easily contribute changes but I decided to just start mucking around however I want and worry about it later. e.g. I'm going to change getHeight to getAscent instead of trying to work around it. I'll also delete that decrement and see if anything breaks, lol.
Makes sense. You may also consider just creating a new Element that works the way you want.
I guess that doesn't work for changing all the elements that use label already.
That does highlight an interesting point with respect to components like textareas. Specifying a font isn't enough.
I'm not sure what you mean. Do you mean that we could have a protocol for font that provides ascent, descent, advance, etc instead of trying to get it off of the font?
What I'm thinking seems clearer to me now. The origin of text in the java2d backend is a point on the baseline at the far left end. text-bounds
needs to shift the origin up to be at the top left of the text so that you can draw some text and draw a box that bounds the text and have the text looks like it's compactly nested inside the box. That distance is the ascent. The height would include the descent which isn't relevant for this.
Recognizing that we need to translate the origin of the font, maybe we make that configurable. The caller can decide how to translate the origin and can use height or ascent or ascent+leading. Just pass a function. I might not want to make the origin the top left; maybe I pick a good y-coordinate for the baseline of the font in my button. Or maybe I use the strikethrough as the center...
If I consider the origin as the top left of the em block then height is right... It's just not what I want at the moment.
What do you think about unit tests that compare backends? For stuff like this^ we might want a test...
Oh, right. Height would not give you the top of the em block. I'm talking in circles. I'm going to make some examples.
Right now, if someone uses textarea
, they can provide a font which provides a name, size, and weight. If someone wanted to reuse textarea
with different styling they would probably want more options. I don't know what the right answer is off the top of my head. It would require some design work.
> What do you think about unit tests that compare backends? For stuff like this^ we might want a test... Being able to compare backends makes sense, but having different backends exactly match is a non-goal.
There are many reasons why different backends might have different output.
For some backends (like the terminal backend), there's not even a good way to compare it with other backends.
Another point is that all the current backends are based on 2d graphics, but all the membrane concepts of UI can be applied to any output (eg. 3d, audio, haptic, smell-o-vision, etc).
https://docs.oracle.com/en/java/javase/16/docs/api/java.desktop/java/awt/font/TextLayout.html
Constructing and drawing a TextLayout and its bounding rectangle:
Graphics2D g = ...;
Point2D loc = ...;
Font font = Font.getFont("Helvetica-bold-italic");
FontRenderContext frc = g.getFontRenderContext();
TextLayout layout = new TextLayout("This is a string", font, frc);
layout.draw(g, (float)loc.getX(), (float)loc.getY());
Rectangle2D bounds = layout.getBounds();
bounds.setRect(bounds.getX()+loc.getX(),
bounds.getY()+loc.getY(),
bounds.getWidth(),
bounds.getHeight());
g.draw(bounds);
The TextLayout docs has an example on how to get the bounds. That might be easier/nicer than folding Font.getStringBounds against max
. I'm having trouble getting it to work for me though so I don't know that I want to promote it. Just fyi I guess.
(defn line-metrics
[{:keys [font text]}]
(let [jfont (backend/get-java-font font)
frc (backend/get-font-render-context)
line-metrics (.getLineMetrics jfont
text
frc)
string-bounds (.getStringBounds jfont text frc)
text-layout (TextLayout. text jfont frc)]
{:line-metrics {:height (.getHeight line-metrics)
:ascent (.getAscent line-metrics)
:descent (.getDescent line-metrics)
:leading (.getLeading line-metrics)
:strikethrough-offset (.getStrikethroughOffset line-metrics)
:strikethrough-thickness (.getStrikethroughThickness line-metrics)
:underline-offset (.getUnderlineOffset line-metrics)
:underline-thickness (.getUnderlineThickness line-metrics)}
:string-bounds {:width (.getWidth string-bounds)
:height (.getHeight string-bounds)}
:text-layout {:origin-x (.getX (.getBounds text-layout))
:origin-y (.getY (.getBounds text-layout))
:width (.getWidth (.getBounds text-layout))
:height (.getHeight (.getBounds text-layout))}}))
(line-metrics (ui/label "SUBMIT" (ui/font nil 40)))
{:line-metrics {:height 50.3125
:ascent 40.214844
:descent 8.7890625
:leading 1.3085938
:strikethrough-offset -10.3515625
:strikethrough-thickness 1.9921875
:underline-offset 4.2382812
:underline-thickness 2.9296875}
:string-bounds {:width 151.11328125
:height 50.3125}
:text-layout {:origin-x 1.796875
:origin-y -29.125
:width
:height 29.609375}}
(let [lbl (ui/label "SUBMIT" (ui/font nil 40))
jfont (backend/get-java-font (:font lbl))
text (:text lbl)]
(backend/text-bounds jfont text))
[151.11328125 50.3125]
(line-metrics (ui/label "SUBMIT\nBUTTON" (ui/font nil 40)))
{:line-metrics {:height 50.3125
:ascent 40.214844
:descent 8.7890625
:leading 1.3085938
:strikethrough-offset -10.3515625
:strikethrough-thickness 1.9921875
:underline-offset 4.2382812
:underline-thickness 2.9296875}
:string-bounds {:width 315.546875
:height 50.3125}
:text-layout {:origin-x 1.796875
:origin-y -29.140625
:width 310.47265625
:height 29.625}}
(let [lbl (ui/label "SUBMIT\nBUTTON" (ui/font nil 40))
jfont (backend/get-java-font (:font lbl))
text (:text lbl)]
(backend/text-bounds jfont text))
[164.43359375 100.625]
I'm still trying to understand these numbers.With syntax highlighting... https://gist.github.com/rgkirch/5242a58ca7d5ff4ff32f42a9f84247d2
Not sure what your goal is with this experiment, but if you're trying to find a better API, I've been thinking that figma’s data model might provide some good inspiration. Additionally, one of the primary use cases is aligning text with other elements. You should be able to align with baseline, ascent, or in rare circumstances, other features.
Sorry, yea. I never followed up with what I thought about these numbers. e.g. I don't know how to interpret [:text-layout :height] Yea, I was hoping it would be a better api. Where can I read about Figma's data model?
https://www.figma.com/developers/api#node-types It's a little wonky, but I think it has proven that it can represent the types of things UI designers care about.