Repository: JuliaPlots/Makie.jl (julia)

Issue:
Allow `scatter` to accept keyword `font`
When using unicode characters as markers for a `scatter` plot, it is convinient to be able to provide a font to choose the characters from, since the default font may be missing some.

Following the discussion on Slack, Simon mentioned:

---

It tries to find the font in AbstractPlotting._alternative_fonts

https://github.com/JuliaPlots/AbstractPlotting.jl/blob/master/src/utilities/texture_atlas.jl#L145

that's the code figuring out, what font to use to render a particular unicode character in the scatter

I guess you could manually load a font with FreeTypeAbstration.FTFont and push it to that array

---

This process is a bit too low level and I think it would be more safe if this (or a similar) process was supported via a `font` keyword instead.

Pull request description:
# Description

Probably fixes #4830
Closes #620

The code from the first commit always picked the last font alternative because it was missing a `break`. CairoMakie picks the first working alternative, as it should. `find_font_for_char` does too, so this should equalize GLMakie and CairoMakie. (Both char markers from #4830 run into this issue.)

TODO:
- [x] add `font` to scatter (because CairoMakie, GLMakie and WGLMakie all suggest it should be there)
- [x] add tests
- [x] Figure out what to do with theme inheritance


## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist

- [x] Added an entry in CHANGELOG.md (for new features and breaking changes)
- [x] Added or changed relevant sections in the documentation
- [ ] Added unit tests for new algorithms, conversion methods, etc.
- [x] Added reference image tests for new plotting functions, recipes, visual options, etc.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b931a41561..21f92d46bbb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## [Unreleased]
 
+- Added `font` attribute and fixed faulty selection in `scatter`. Scatter fonts can now be themed with `markerfont`. [#4832](https://github.com/MakieOrg/Makie.jl/pull/4832)
 - Fixed categorical `cgrad` interpolating at small enough steps [#4858](https://github.com/MakieOrg/Makie.jl/pull/4858)
 
 ## [0.22.2] - 2025-02-26
diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl
index 8c4bf553445..c000b5d2490 100644
--- a/GLMakie/src/drawing_primitives.jl
+++ b/GLMakie/src/drawing_primitives.jl
@@ -451,7 +451,7 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Union{Sca
                         return nothing
                     end
                 end
-                font = get(gl_attributes, :font, Observable(Makie.defaultfont()))
+                font = map(to_font, pop!(gl_attributes, :font))
                 gl_attributes[:uv_offset_width][] == Vec4f(0) && delete!(gl_attributes, :uv_offset_width)
                 get!(gl_attributes, :uv_offset_width) do
                     return Makie.primitive_uv_offset_width(atlas, marker, font)
diff --git a/MakieCore/src/basic_plots.jl b/MakieCore/src/basic_plots.jl
index ad345528bbe..12af5ab0e92 100644
--- a/MakieCore/src/basic_plots.jl
+++ b/MakieCore/src/basic_plots.jl
@@ -484,6 +484,8 @@ Plots a marker for each element in `(x, y, z)`, `(x, y)`, or `positions`.
     marker_offset = Vec3f(0)
     "Controls whether the model matrix (without translation) applies to the marker itself, rather than just the positions. (If this is true, `scale!` and `rotate!` will affect the marker."
     transform_marker = false
+    "Sets the font used for character markers. Can be a `String` specifying the (partial) name of a font or the file path of a font file"
+    font = @inherit markerfont
     "Optional distancefield used for e.g. font and bezier path rendering. Will get set automatically."
     distancefield = nothing
     uv_offset_width = (0.0, 0.0, 0.0, 0.0)
diff --git a/WGLMakie/src/particles.jl b/WGLMakie/src/particles.jl
index be2978ee6d5..7cefbe43c78 100644
--- a/WGLMakie/src/particles.jl
+++ b/WGLMakie/src/particles.jl
@@ -174,7 +174,7 @@ function scatter_shader(scene::Scene, attributes, plot)
     marker = nothing
     atlas = wgl_texture_atlas()
     if haskey(attributes, :marker)
-        font = get(attributes, :font, Observable(Makie.defaultfont()))
+        font = map(Makie.to_font, pop!(attributes, :font))
         marker = lift(plot, attributes[:marker]) do marker
             marker isa Makie.FastPixel && return Rect # FastPixel not supported, but same as Rect just slower
             marker isa AbstractMatrix{<:Colorant} && return to_color(marker)
diff --git a/src/conversions.jl b/src/conversions.jl
index ea4bcdf680d..5510df78f4d 100644
--- a/src/conversions.jl
+++ b/src/conversions.jl
@@ -1442,6 +1442,8 @@ end
 
 to_font(fonts::Attributes, x) = to_font(x)
 
+to_font(::Automatic) = defaultfont()
+
 
 """
     rotation accepts:
diff --git a/src/layouting/data_limits.jl b/src/layouting/data_limits.jl
index 113907e0c78..2997ab6fab5 100644
--- a/src/layouting/data_limits.jl
+++ b/src/layouting/data_limits.jl
@@ -89,7 +89,7 @@ function data_limits(plot::Scatter)
             get_texture_atlas(),
             plot.marker[],
             plot.markersize[],
-            get(plot.attributes, :font, Observable(Makie.defaultfont())),
+            to_font(plot.font[]),
             plot
         )
         rotations = convert_attribute(to_value(get(plot, :rotation, 0)), key"rotation"())
diff --git a/src/theming.jl b/src/theming.jl
index a4aebd95c33..343fda08262 100644
--- a/src/theming.jl
+++ b/src/theming.jl
@@ -45,6 +45,7 @@ const MAKIE_DEFAULT_THEME = Attributes(
     markercolor = :black,
     markerstrokecolor = :black,
     markerstrokewidth = 0,
+    markerfont = "TeX Gyre Heros Makie",
     linecolor = :black,
     linewidth = 1.5,
     linestyle = nothing,
diff --git a/src/utilities/texture_atlas.jl b/src/utilities/texture_atlas.jl
index 2924d312b9c..bf33be12f27 100644
--- a/src/utilities/texture_atlas.jl
+++ b/src/utilities/texture_atlas.jl
@@ -256,7 +256,7 @@ function find_font_for_char(glyph, font::NativeFont)
     FreeTypeAbstraction.glyph_index(font, glyph) != 0 && return font
     # it seems that linebreaks are not found which messes up font metrics
     # if another font is selected just for those chars
-    glyph in ('\n', '\r', '\t') && return font
+    glyph in ('\n', '\r', '\t', UInt32(0)) && return font
     for afont in alternativefonts()
         if FreeTypeAbstraction.glyph_index(afont, glyph) != 0
             return afont
@@ -266,14 +266,7 @@ function find_font_for_char(glyph, font::NativeFont)
 end
 
 function glyph_index!(atlas::TextureAtlas, glyph, font::NativeFont)
-    if FreeTypeAbstraction.glyph_index(font, glyph) == 0
-        for afont in alternativefonts()
-            if FreeTypeAbstraction.glyph_index(afont, glyph) != 0
-                font = afont
-            end
-        end
-    end
-    return insert_glyph!(atlas, glyph, font)
+    return insert_glyph!(atlas, glyph, find_font_for_char(glyph, font))
 end
 
 function glyph_index!(atlas::TextureAtlas, b::BezierPath)


Test patch:
diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl
index cbe6018e6ec..92abd4897f1 100644
--- a/ReferenceTests/src/tests/primitives.jl
+++ b/ReferenceTests/src/tests/primitives.jl
@@ -1161,3 +1161,22 @@ end
 
     fig
 end
+
+@reference_test "Scatter fonts" begin
+    scene = Scene(size = (150, 150), camera = campixel!)
+
+    # Just needs to not be Fira Mona here, but good to test the default too
+    @test Makie.to_font(Makie.automatic) == Makie.to_font("TeX Gyre Heros Makie")
+
+    scatter!(scene, (40,  40), marker=Rect, markersize=45, color = :black, strokecolor = :red, strokewidth = 1)
+    scatter!(scene, (40,  40), marker='◇', markersize=45, color = :white)
+    scatter!(scene, (110, 40), marker=Rect, markersize=45, color = :green, strokecolor = :red, strokewidth = 1)
+    text!(scene,    (110, 40), text = "◇", fontsize = 45, align = (:center, :center), color = :white)
+
+    scatter!(scene, (40,  110), marker=Rect, font = "Fira Mono", markersize=45, color = :black, strokecolor = :red, strokewidth = 1)
+    scatter!(scene, (40,  110), marker='◇',  font = "Fira Mono", markersize=45, color = :white)
+    scatter!(scene, (110, 110), marker=Rect, font = "Fira Mono", markersize=45, color = :green, strokecolor = :red, strokewidth = 1)
+    text!(scene,    (110, 110), text = "◇",  font = "Fira Mono", fontsize = 45, align = (:center, :center), color = :white)
+
+    scene
+end
\ No newline at end of file