Skip to content

Commit

Permalink
Subscript/superscript combinations (#4489)
Browse files Browse the repository at this point in the history
* Add right and left subsup rich text

* make text a little smaller again

* tweak positioning

* add to rich text test

* revert to old sizing

* add changelog

* add example to docs page
  • Loading branch information
jkrumbiegel authored Oct 29, 2024
1 parent 13ac70c commit fd41dd4
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 30 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]

- Added `subsup` and `left_subsup` functions that offer stacked sub- and superscripts for `rich` text which means this style can be used with arbitrary fonts and is not limited to fonts supported by MathTeXEngine.jl [#4489](https://github.com/MakieOrg/Makie.jl/pull/4489).

## [0.21.15] - 2024-10-25

- Allowed creation of `Legend` with entries that have no legend elements [#4526](https://github.com/MakieOrg/Makie.jl/pull/4526).
Expand Down
8 changes: 6 additions & 2 deletions ReferenceTests/src/tests/figures_and_makielayout.jl
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,12 @@ end
xlabel = rich("X", subscript("label", fontsize = 25)),
ylabel = rich("Y", superscript("label")),
)
Label(f[1, 2], rich("Hi", rich("Hi", offset = (0.2, 0.2), color = :blue)), tellheight = false)
Label(f[1, 3], rich("X", superscript("super"), subscript("sub")), tellheight = false)
gl = GridLayout(f[1, 2], tellheight = false)
Label(gl[1, 1], rich("Hi", rich("Hi", offset = (0.2, 0.2), color = :blue)))
Label(gl[2, 1], rich("X", superscript("super"), subscript("sub")))
Label(gl[3, 1], rich(left_subsup("92", "238"), "U"))
Label(gl[4, 1], rich("SO", subsup("4", "2−")))
Label(gl[5, 1], rich("x", subsup("f", "g")))
f
end

Expand Down
6 changes: 4 additions & 2 deletions docs/src/reference/plots/text.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,9 @@ f
## Rich text

With rich text, you can conveniently plot text whose parts have different colors or fonts, and you can position sections as subscripts and superscripts.
You can create such rich text objects using the functions `rich`, `superscript` and `subscript`, all of which create `RichText` objects.
You can create such rich text objects using the functions `rich`, `superscript`, `subscript`, `subsup` and `left_subsup`, all of which create `RichText` objects.

Each of these functions takes a variable number of arguments, each of which can be a `String` or `RichText`.
Each of these functions takes a variable number of arguments (except `subsup` and `left_subsup` which take exactly two arguments), each of which can be a `String` or `RichText`.
Each can also take keyword arguments such as `color` or `font`, to set these attributes for the given part.
The top-level settings for font, color, etc. are taken from the `text` attributes as usual.

Expand All @@ -221,6 +221,8 @@ end
Label(f[2, 1], rich(rainbow_chars...), font = :bold)
Label(f[3, 1], rich("Chemists use notations like ", left_subsup("92", "238"), "U or PO", subsup("4", "3−")))
f
```

Expand Down
140 changes: 114 additions & 26 deletions src/basic_recipes/text.jl
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,40 @@ function Base.show(io::IO, ::MIME"text/plain", r::RichText)
print(io, "RichText: \"$(String(r))\"")
end

"""
rich(args...; kwargs...)
Create a `RichText` object containing all elements in `args`.
"""
rich(args...; kwargs...) = RichText(:span, args...; kwargs...)
"""
subscript(args...; kwargs...)
Create a `RichText` object representing a superscript containing all elements in `args`.
"""
subscript(args...; kwargs...) = RichText(:sub, args...; kwargs...)
"""
superscript(args...; kwargs...)
Create a `RichText` object representing a superscript containing all elements in `args`.
"""
superscript(args...; kwargs...) = RichText(:sup, args...; kwargs...)
"""
subsup(subscript, superscript; kwargs...)
Create a `RichText` object representing a right subscript/superscript combination,
where both scripts are left-aligned against the preceding text.
"""
subsup(args...; kwargs...) = RichText(:subsup, args...; kwargs...)
"""
left_subsup(subscript, superscript; kwargs...)
Create a `RichText` object representing a left subscript/superscript combination,
where both scripts are right-aligned against the following text.
"""
left_subsup(args...; kwargs...) = RichText(:leftsubsup, args...; kwargs...)

export rich, subscript, superscript
export rich, subscript, superscript, subsup, left_subsup

function _get_glyphcollection_and_linesegments(rt::RichText, index, ts, f, fset, al, rot, jus, lh, col, scol, swi, www, offs)
gc = layout_text(rt, ts, f, fset, al, rot, jus, lh, col)
Expand Down Expand Up @@ -381,11 +410,11 @@ function layout_text(rt::RichText, ts, f, fset, al, rot, jus, lh, col)

_f = to_font(fset, f)

stack = [GlyphState(0, 0, Vec2f(ts), _f, to_color(col))]

lines = [GlyphInfo[]]

process_rt_node!(stack, lines, rt, fset)
gs = GlyphState(0, 0, Vec2f(ts), _f, to_color(col))

process_rt_node!(lines, gs, rt, fset)

apply_lineheight!(lines, lh)
apply_alignment_and_justification!(lines, jus, al)
Expand Down Expand Up @@ -463,23 +492,64 @@ function float_justification(ju, al)::Float32
end
end

function process_rt_node!(stack, lines, rt::RichText, fonts)
_type(x) = nothing
_type(r::RichText) = r.type
function process_rt_node!(lines, gs::GlyphState, rt::RichText, fonts)
T = Val(rt.type)

if T === Val(:subsup) || T === Val(:leftsubsup)
if length(rt.children) != 2
throw(ArgumentError("Found subsup rich text with $(length(rt.children)) which has to have exactly 2 children instead. The children were: $(rt.children)"))
end
sub, sup = rt.children
sub_lines = Vector{GlyphInfo}[[]]
new_gs_sub = new_glyphstate(gs, rt, Val(:subsup_sub), fonts)
new_gs_sub_post = process_rt_node!(sub_lines, new_gs_sub, sub, fonts)
sup_lines = Vector{GlyphInfo}[[]]
new_gs_sup = new_glyphstate(gs, rt, Val(:subsup_sup), fonts)
new_gs_sup_post = process_rt_node!(sup_lines, new_gs_sup, sup, fonts)
if length(sub_lines) != 1
error("It is not allowed to include linebreaks in a subsup rich text element, the invalid element was: $(repr(sub))")
end
if length(sup_lines) != 1
error("It is not allowed to include linebreaks in a subsup rich text element, the invalid element was: $(repr(sup))")
end
sub_line = only(sub_lines)
sup_line = only(sup_lines)
if T === Val(:leftsubsup)
right_align!(sub_line, sup_line)
end
append!(lines[end], sub_line)
append!(lines[end], sup_line)
x = max(new_gs_sub_post.x, new_gs_sup_post.x)
else
new_gs = new_glyphstate(gs, rt, T, fonts)
for (i, c) in enumerate(rt.children)
new_gs = process_rt_node!(lines, new_gs, c, fonts)
end
x = new_gs.x
end

return GlyphState(x, gs.baseline, gs.size, gs.font, gs.color)
end

push!(stack, new_glyphstate(stack[end], rt, Val(rt.type), fonts))
for (i, c) in enumerate(rt.children)
process_rt_node!(stack, lines, c, fonts)
function right_align!(line1::Vector{GlyphInfo}, line2::Vector{GlyphInfo})
isempty(line1) || isempty(line2) && return
xmax1, xmax2 = map((line1, line2)) do line
maximum(line; init = 0f0) do ginfo
GlyphInfo
ginfo.origin[1] + ginfo.size[1] * (ginfo.extent.ink_bounding_box.origin[1] + ginfo.extent.ink_bounding_box.widths[1])
end
end
line_to_shift = xmax1 > xmax2 ? line2 : line1
for j in eachindex(line_to_shift)
l = line_to_shift[j]
o = l.origin
l = GlyphInfo(l; origin = o .+ Point2f(abs(xmax2 - xmax1), 0))
line_to_shift[j] = l
end
gs = pop!(stack)
gs_top = stack[end]
# x needs to continue even if going a level up
stack[end] = GlyphState(gs.x, gs_top.baseline, gs_top.size, gs_top.font, gs_top.color)
return
end

function process_rt_node!(stack, lines, s::String, _)
gs = stack[end]
function process_rt_node!(lines, gs::GlyphState, s::String, _)
y = gs.baseline
x = gs.x
for char in s
Expand All @@ -505,20 +575,15 @@ function process_rt_node!(stack, lines, s::String, _)
x = x + gext.hadvance * gs.size[1]
end
end
stack[end] = GlyphState(x, y, gs.size, gs.font, gs.color)
return
end

function new_glyphstate(gs::GlyphState, rt::RichText, val::Val, fonts)
gs
return GlyphState(x, y, gs.size, gs.font, gs.color)
end

_get_color(attributes, default)::RGBAf = haskey(attributes, :color) ? to_color(attributes[:color]) : default
_get_font(attributes, default::NativeFont, fonts)::NativeFont = haskey(attributes, :font) ? to_font(fonts, attributes[:font]) : default
_get_fontsize(attributes, default)::Vec2f = haskey(attributes, :fontsize) ? Vec2f(to_fontsize(attributes[:fontsize])) : default
_get_offset(attributes, default)::Vec2f = haskey(attributes, :offset) ? Vec2f(attributes[:offset]) : default

function new_glyphstate(gs::GlyphState, rt::RichText, val::Val{:sup}, fonts)
function new_glyphstate(gs::GlyphState, rt::RichText, ::Val{:sup}, fonts)
att = rt.attributes
fontsize = _get_fontsize(att, gs.size * 0.66)
offset = _get_offset(att, Vec2f(0)) .* fontsize
Expand All @@ -531,7 +596,7 @@ function new_glyphstate(gs::GlyphState, rt::RichText, val::Val{:sup}, fonts)
)
end

function new_glyphstate(gs::GlyphState, rt::RichText, val::Val{:span}, fonts)
function new_glyphstate(gs::GlyphState, rt::RichText, ::Val{:span}, fonts)
att = rt.attributes
fontsize = _get_fontsize(att, gs.size)
offset = _get_offset(att, Vec2f(0)) .* fontsize
Expand All @@ -544,13 +609,36 @@ function new_glyphstate(gs::GlyphState, rt::RichText, val::Val{:span}, fonts)
)
end

function new_glyphstate(gs::GlyphState, rt::RichText, val::Val{:sub}, fonts)
function new_glyphstate(gs::GlyphState, rt::RichText, ::Val{:sub}, fonts)
att = rt.attributes
fontsize = _get_fontsize(att, gs.size * 0.66)
offset = _get_offset(att, Vec2f(0)) .* fontsize
GlyphState(
gs.x + offset[1],
gs.baseline - 0.15 * gs.size[2] + offset[2],
gs.baseline - 0.25 * gs.size[2] + offset[2],
fontsize,
_get_font(att, gs.font, fonts),
_get_color(att, gs.color),
)
end

function new_glyphstate(gs::GlyphState, rt::RichText, ::Val{:subsup_sub}, fonts)
att = rt.attributes
fontsize = _get_fontsize(att, gs.size * 0.66)
GlyphState(
gs.x,
gs.baseline - 0.25 * gs.size[2],
fontsize,
_get_font(att, gs.font, fonts),
_get_color(att, gs.color),
)
end
function new_glyphstate(gs::GlyphState, rt::RichText, ::Val{:subsup_sup}, fonts)
att = rt.attributes
fontsize = _get_fontsize(att, gs.size * 0.66)
GlyphState(
gs.x,
gs.baseline + 0.4 * gs.size[2],
fontsize,
_get_font(att, gs.font, fonts),
_get_color(att, gs.color),
Expand Down

0 comments on commit fd41dd4

Please sign in to comment.