Skip to content

Commit 565a163

Browse files
committed
Allow hard/soft nothings in Face, optimise layout
TODO: elaborate on the justification and methodology.
1 parent c404a47 commit 565a163

File tree

2 files changed

+266
-123
lines changed

2 files changed

+266
-123
lines changed

src/faces.jl

Lines changed: 180 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,20 @@ function Base.parse(::Type{SimpleColor}, rgb::String)
8383
color
8484
end
8585

86+
struct _Face
87+
font::String
88+
height::UInt64
89+
weight::Symbol
90+
slant::Symbol
91+
foreground::SimpleColor
92+
background::SimpleColor
93+
underline::SimpleColor
94+
underline_style::Symbol
95+
strikethrough::UInt8
96+
inverse::UInt8
97+
inherit::Memory{Symbol}
98+
end
99+
86100
"""
87101
A [`Face`](@ref) is a collection of graphical attributes for displaying text.
88102
Faces control how text is displayed in the terminal, and possibly other
@@ -121,20 +135,53 @@ All attributes can be set via the keyword constructor, and default to `nothing`.
121135
- `inherit` (a `Vector{Symbol}`): Names of faces to inherit from,
122136
with earlier faces taking priority. All faces inherit from the `:default` face.
123137
"""
124-
struct Face
125-
font::Union{Nothing, String}
126-
height::Union{Nothing, Int, Float64}
127-
weight::Union{Nothing, Symbol}
128-
slant::Union{Nothing, Symbol}
129-
foreground::Union{Nothing, SimpleColor}
130-
background::Union{Nothing, SimpleColor}
131-
underline::Union{Nothing, Bool, SimpleColor,
132-
Tuple{<:Union{Nothing, SimpleColor}, Symbol}}
133-
strikethrough::Union{Nothing, Bool}
134-
inverse::Union{Nothing, Bool}
135-
inherit::Vector{Symbol}
138+
mutable struct Face # We want to force reference semantics here
139+
const f::_Face
136140
end
137141

142+
# NOTE: We use shenanigans instead of `Union{Nothing, X}` in the
143+
# face fields for two reasons:
144+
# 1. To improve the memory layout of `Face`, so there's less indirection
145+
# 2. To allow for having two flavours of `nothing`, "weak" and "strong".
146+
# This allows for us to distinguish between unset attributes (weak)
147+
# and attributes that should replace a set value with weak nothing (strong).
148+
# We can only do this because we have full knowledge and dominion
149+
# over the semantics of `Face`.
150+
151+
const WEAK_NOTHING_SYMB = gensym("nothing")
152+
const WEAK_NOTHING_STR = String(WEAK_NOTHING_SYMB)
153+
154+
weaknothing(::Type{Symbol}) = WEAK_NOTHING_SYMB
155+
weaknothing(::Type{String}) = WEAK_NOTHING_STR
156+
weaknothing(::Type{N}) where {N<:Number} = -one(N)
157+
weaknothing(::Type{SimpleColor}) = SimpleColor(WEAK_NOTHING_SYMB)
158+
weaknothing(::Type{Bool}) = strongnothing(UInt8)
159+
weaknothing(::Type) = nothing
160+
weaknothing(x) = weaknothing(typeof(x))
161+
162+
isweaknothing(x) = x === weaknothing(typeof(x))
163+
isweaknothing(s::String) = pointer(s) == pointer(WEAK_NOTHING_STR)
164+
isweaknothing(c::SimpleColor) = c.value === WEAK_NOTHING_SYMB
165+
166+
const STRONG_NOTHING_SYMB = gensym("nothing")
167+
const STRONG_NOTHING_STR = String(STRONG_NOTHING_SYMB)
168+
169+
strongnothing(::Type{Symbol}) = STRONG_NOTHING_SYMB
170+
strongnothing(::Type{String}) = STRONG_NOTHING_STR
171+
strongnothing(::Type{N}) where {N<:Number} = - (0x2 * one(N))
172+
strongnothing(::Type{SimpleColor}) = SimpleColor(STRONG_NOTHING_SYMB)
173+
strongnothing(::Type{Bool}) = strongnothing(UInt8)
174+
strongnothing(::Type) = nothing
175+
strongnothing(x) = strongnothing(typeof(x))
176+
177+
isstrongnothing(x) = x === strongnothing(typeof(x))
178+
isstrongnothing(s::String) = pointer(s) == pointer(STRONG_NOTHING_STR)
179+
isstrongnothing(c::SimpleColor) = c.value === STRONG_NOTHING_SYMB
180+
181+
isnothinglike(x) = isweaknothing(x) || isstrongnothing(x)
182+
183+
# With our flavours of nothing defined, we can now define the Face constructor.
184+
138185
function Face(; font::Union{Nothing, String} = nothing,
139186
height::Union{Nothing, Int, Float64} = nothing,
140187
weight::Union{Nothing, Symbol} = nothing,
@@ -143,53 +190,90 @@ function Face(; font::Union{Nothing, String} = nothing,
143190
background = nothing, # nothing, or SimpleColor-able value
144191
underline::Union{Nothing, Bool, SimpleColor,
145192
Symbol, RGBTuple, UInt32,
146-
Tuple{<:Any, Symbol}
147-
} = nothing,
193+
Tuple{<:Any, Symbol}} = nothing,
148194
strikethrough::Union{Nothing, Bool} = nothing,
149195
inverse::Union{Nothing, Bool} = nothing,
150196
inherit::Union{Symbol, Vector{Symbol}} = Symbol[],
151197
_...) # Simply ignore unrecognised keyword arguments.
152-
ascolor(::Nothing) = nothing
198+
inheritlen = if inherit isa Vector length(inherit) else 1 end
199+
inheritlist = if inherit isa Vector
200+
inherit.ref.mem
201+
else
202+
mem = Memory{Symbol}(undef, 1)
203+
mem[1] = inherit
204+
mem
205+
end
206+
ascolor(::Nothing) = SimpleColor(WEAK_NOTHING_SYMB)
153207
ascolor(c::AbstractString) = parse(SimpleColor, c)
154208
ascolor(c::Any) = convert(SimpleColor, c)
155-
Face(font, height, weight, slant,
156-
ascolor(foreground), ascolor(background),
157-
if underline isa Tuple{Any, Symbol}
158-
(ascolor(underline[1]), underline[2])
159-
elseif underline in (:straight, :double, :curly, :dotted, :dashed)
160-
(nothing, underline)
161-
elseif underline isa Bool
162-
underline
163-
else
164-
ascolor(underline)
165-
end,
166-
strikethrough,
167-
inverse,
168-
if inherit isa Symbol
169-
[inherit]
170-
else inherit end)
209+
ul, ulstyle = if underline isa Tuple{Any, Symbol}
210+
ascolor(underline[1]), underline[2]
211+
elseif underline in (:straight, :double, :curly, :dotted, :dashed)
212+
weaknothing(SimpleColor), underline
213+
elseif underline isa Bool
214+
SimpleColor(ifelse(underline, :foreground, :background)), :straight
215+
else
216+
ascolor(underline), :straight
217+
end
218+
f = _Face(something(font, weaknothing(String)),
219+
if isnothing(height)
220+
weaknothing(UInt64)
221+
elseif height isa Float64
222+
reinterpret(UInt64, height) & ~(typemax(UInt64) >> 1)
223+
else
224+
UInt64(height)
225+
end,
226+
something(weight, weaknothing(Symbol)),
227+
something(slant, weaknothing(Symbol)),
228+
ascolor(foreground),
229+
ascolor(background),
230+
ul, ulstyle,
231+
something(strikethrough, weaknothing(Bool)),
232+
something(inverse, weaknothing(Bool)),
233+
inheritlist)
234+
Face(f)
235+
end
236+
237+
function Base.getproperty(face::Face, attr::Symbol)
238+
val = getfield(getfield(face, :f), attr)
239+
if isnothinglike(val)
240+
elseif attr (:strikethrough, :inverse)
241+
if attr != 0x3
242+
attr == 0x1
243+
end
244+
elseif attr == :underline
245+
style = getfield(getfield(face, :f), :underline_style)
246+
val, style
247+
else
248+
val
249+
end
171250
end
172251

252+
Base.propertynames(::Face) = setdiff(fieldnames(_Face), (:underline_style,))
253+
173254
function Base.:(==)(a::Face, b::Face)
174-
a.font == b.font &&
175-
a.height === b.height &&
176-
a.weight == b.weight &&
177-
a.slant == b.slant &&
178-
a.foreground == b.foreground &&
179-
a.background == b.background &&
180-
a.underline == b.underline &&
181-
a.strikethrough == b.strikethrough &&
182-
a.inverse == b.inverse &&
183-
a.inherit == b.inherit
255+
af, bf = getfield(a, :f), getfield(b, :f)
256+
af.font == bf.font &&
257+
af.height === bf.height &&
258+
af.weight == bf.weight &&
259+
af.slant == bf.slant &&
260+
af.foreground == bf.foreground &&
261+
af.background == bf.background &&
262+
af.underline == bf.underline &&
263+
af.strikethrough == bf.strikethrough &&
264+
af.inverse == bf.inverse &&
265+
af.inherit == bf.inherit
184266
end
185267

186268
Base.hash(f::Face, h::UInt) =
187-
mapfoldr(Base.Fix1(getfield, f), hash, fieldnames(Face), init=hash(Face, h))
269+
mapfoldr(Base.Fix1(getfield, getfield(f, :f)), hash, fieldnames(Face), init=hash(Face, h))
188270

189-
Base.copy(f::Face) =
190-
Face(f.font, f.height, f.weight, f.slant,
191-
f.foreground, f.background, f.underline,
192-
f.strikethrough, f.inverse, copy(f.inherit))
271+
function Base.copy(f::Face)
272+
ff = getfield(f, :f)
273+
Face(_Face(ff.font, ff.height, ff.weight, ff.slant,
274+
ff.foreground, ff.background, ff.underline,
275+
ff.strikethrough, ff.inverse, copy(ff.inherit)))
276+
end
193277

194278
"""
195279
merge(initial::StyledStrings.Face, others::StyledStrings.Face...)
@@ -199,31 +283,57 @@ Merge the properties of the `initial` face and `others`, with later faces taking
199283
This is used to combine the styles of multiple faces, and to resolve inheritance.
200284
"""
201285
function Base.merge(a::Face, b::Face)
202-
if isempty(b.inherit)
203-
# Extract the heights to help type inference a bit to be able
204-
# to narrow the types in e.g. `aheight * bheight`
205-
aheight = a.height
206-
bheight = b.height
207-
abheight = if isnothing(bheight) aheight
208-
elseif isnothing(aheight) bheight
209-
elseif bheight isa Int bheight
210-
elseif aheight isa Int round(Int, aheight * bheight)
211-
else aheight * bheight end
212-
Face(if isnothing(b.font) a.font else b.font end,
213-
abheight,
214-
if isnothing(b.weight) a.weight else b.weight end,
215-
if isnothing(b.slant) a.slant else b.slant end,
216-
if isnothing(b.foreground) a.foreground else b.foreground end,
217-
if isnothing(b.background) a.background else b.background end,
218-
if isnothing(b.underline) a.underline else b.underline end,
219-
if isnothing(b.strikethrough) a.strikethrough else b.strikethrough end,
220-
if isnothing(b.inverse) a.inverse else b.inverse end,
221-
a.inherit)
286+
af, bf = getfield(a, :f), getfield(b, :f)
287+
function mergeattr(a₀, b₀, attr::Symbol)
288+
a′ = getfield(a₀, attr)
289+
b′ = getfield(b₀, attr)
290+
if isweaknothing(b′)
291+
a′
292+
elseif isstrongnothing(b′)
293+
weaknothing(b′)
294+
else
295+
b′
296+
end
297+
end
298+
if isempty(bf.inherit)
299+
abheight = if isstrongnothing(bf.height)
300+
weaknothing(bf.height)
301+
elseif isweaknothing(bf.height)
302+
af.height
303+
elseif isnothinglike(af.height)
304+
bf.height
305+
elseif iszero(bf.height & ~(typemax(UInt64) >> 1)) # bf.height::Int
306+
bf.height
307+
elseif iszero(af.height & ~(typemax(UInt64) >> 1)) # af.height::Int
308+
aint = reinterpret(Int64, af.height) % UInt
309+
bfloat = reinterpret(Float64, bf.height & (typemax(UInt64) >> 1))
310+
aint * bfloat
311+
else # af.height::Float64, bf.height::Float64
312+
afloat = reinterpret(Float64, af.height & (typemax(UInt64) >> 1))
313+
bfloat = reinterpret(Float64, bf.height & (typemax(UInt64) >> 1))
314+
afloat * bfloat
315+
end
316+
Face(_Face(
317+
mergeattr(af, bf, :font),
318+
abheight,
319+
mergeattr(af, bf, :weight),
320+
mergeattr(af, bf, :slant),
321+
mergeattr(af, bf, :foreground),
322+
mergeattr(af, bf, :background),
323+
mergeattr(af, bf, :underline),
324+
if isweaknothing(bf.underline_style)
325+
af.underline_style
326+
else
327+
bf.underline_style
328+
end,
329+
mergeattr(af, bf, :strikethrough),
330+
mergeattr(af, bf, :inverse),
331+
af.inherit))
222332
else
223-
b_noinherit = Face(
224-
b.font, b.height, b.weight, b.slant, b.foreground, b.background,
225-
b.underline, b.strikethrough, b.inverse, Symbol[])
226-
b_inheritance = map(fname -> get(Face, FACES.current[], fname), Iterators.reverse(b.inherit))
333+
b_noinherit = Face(_Face(
334+
bf.font, bf.height, bf.weight, bf.slant, bf.foreground, bf.background,
335+
bf.underline, bf.strikethrough, bf.inverse, Symbol[]))
336+
b_inheritance = map(fname -> get(Face, FACES.current[], fname), Iterators.reverse(bf.inherit))
227337
b_resolved = merge(foldl(merge, b_inheritance), b_noinherit)
228338
merge(a, b_resolved)
229339
end

0 commit comments

Comments
 (0)