// // Copyright (c) 2008, Brian Frank and Andy Frank // Licensed under the Academic Free License version 3.0 // // History: // 16 Jun 2008 Brian Frank Creation // 29 Mar 2017 Brian Frank Refactor for predefined font metrics // ** ** Font models font-family, font-size, and font-style, and font-weight. ** Metrics are available for a predefined set of fonts. ** @Js @Serializable { simple = true } const class Font { ////////////////////////////////////////////////////////////////////////// // Construction ////////////////////////////////////////////////////////////////////////// ** Construct with it-block new make(|This| f) { f(this) } ** Construct a Font with individual fields @NoDoc new makeFields(Str[] names, Float size, FontWeight weight := FontWeight.normal, FontStyle style := FontStyle.normal) { if (names.isEmpty) throw ArgErr("No names specified") this.names = names this.size = size this.weight = weight this.style = style this.data = FontData.find(this) } ** Parse font from string using CSS shorthand format for ** supported properties: ** ** [<style>] [<weight>] <size> <names> ** ** Examples: ** Font.fromStr("12pt Arial") ** Font.fromStr("bold 10pt Courier") ** Font.fromStr("italic bold 8pt Times") ** Font.fromStr("italic 300 10pt sans-serif") static Font? fromStr(Str s, Bool checked := true) { try { toks := s.split toki := 0 style := FontStyle.decode(toks[toki], false) if (style != null) toki++ else style = FontStyle.normal weight := FontWeight.decode(toks[toki], false) if (weight != null) toki++ else weight = FontWeight.normal if (!toks[toki].endsWith("pt")) throw Err() size := toks[toki][0..-3].toFloat toki++ names := decodeNames(toks[toki..-1].join(" ")) return makeFields(names, size, weight, style) } catch (Err e) {} if (checked) throw ParseErr("Invalid Font: $s") return null } private static Str[] decodeNames(Str s) { s.split(',') } private static Float decodeSize(Str s) { if (!s.endsWith("pt")) throw Err("Invalid font size: $s") return s[0..-3].toFloat } private static FontWeight decodeWeight(Str s) { FontWeight.decode(s) } private static FontStyle decodeStyle(Str s) { FontStyle.decode(s) } ////////////////////////////////////////////////////////////////////////// // Props ////////////////////////////////////////////////////////////////////////// ** Construct from a map of CSS props such as font-family, font-size. ** Also see `toProps`. static Font? fromProps([Str:Str] props) { if (props["font-family"] == null) return null return makeFields( decodeNames(props["font-family"] ?: "sans-serif"), decodeSize(props["font-size"] ?: "12pt"), decodeWeight(props["font-weight"] ?: "normal"), decodeStyle(props["font-style"] ?: "normal")) } ** Get CSS style properties for this font. ** Also see `fromProps` Str:Str toProps() { acc := OrderedMap<Str,Str>() acc["font-family"] = names.join(",") acc["font-size"] = GeomUtil.formatFloat(size) + "pt" if (!weight.isNormal) acc["font-weight"] = weight.num.toStr if (!style.isNormal) acc["font-style"] = style.name return acc } ////////////////////////////////////////////////////////////////////////// // Font ////////////////////////////////////////////////////////////////////////// ** First family name in `names` Str name() { names.first } ** List of prioritized family names const Str[] names := ["sans-serif"] ** Size of font in points. const Float size := 11f ** Weight as number from 100 to 900 const FontWeight weight := FontWeight.normal ** Style as normal, italic, or oblique const FontStyle style := FontStyle.normal ////////////////////////////////////////////////////////////////////////// // Identity ////////////////////////////////////////////////////////////////////////// ** Return hash of all fields override Int hash() { names.hash.xor(size.hash).xor(weight.hash * 73).xor(style.hash * 19) } ** Equality is based on all fields. override Bool equals(Obj? that) { x := that as Font if (x == null) return false return names == x.names && size == x.size && weight == x.weight && style == x.style } ** Format as '"[style] [weight] <size>pt <names>"' override Str toStr() { s := StrBuf() if (!style.isNormal) s.add(style.name).addChar(' ') if (!weight.isNormal) s.add(weight.num).addChar(' ') s.add(GeomUtil.formatFloat(size)).add("pt").addChar(' ') s.add(names.join(",")) return s.toStr } ////////////////////////////////////////////////////////////////////////// // Utils ////////////////////////////////////////////////////////////////////////// ** Return this font with different point size. Font toSize(Float size) { if (this.size == size) return this return Font.makeFields(names, size, weight, style) } ** Return this font with different style Font toStyle(FontStyle style) { if (this.style == style) return this return Font.makeFields(names, size, weight, style) } ** Return this font with different weight. Font toWeight(FontWeight weight) { if (this.weight == weight) return this return Font.makeFields(names, size, weight, style) } ////////////////////////////////////////////////////////////////////////// // Metrics ////////////////////////////////////////////////////////////////////////// ** ** DO NOT USE - this design is deprecated in favor of Graphics.metrics ** ** Normalize to the closest font with metrics ** @NoDoc Font normalize() { if (data != null) return this return FontData.normalize(this) } ** ** DO NOT USE - this design is deprecated in favor of Graphics.metrics ** ** Get font metrics for this font. If this is not a [normalized]`normalize` ** font with built-in metrics, then raise UnsupportedErr. ** @NoDoc FontMetrics metrics(DeviceContext dc := DeviceContext.cur) { if (data == null) throw UnsupportedErr("FontMetrics not supported: $this") return FontDataMetrics(dc, size, data) } ** Font metric data from predefined registry private const FontData? data } ************************************************************************** ** FontMetrics ************************************************************************** ** ** FontMetrics represents font size information for a `Font` within ** a specific graphics context. ** @Js abstract const class FontMetrics { ** Get height of this font which is the sum of ** ascent, descent, and leading. abstract Float height() ** Get ascent of this font which is the distance from ** baseline to top of chars, not including any leading area. abstract Float ascent() ** Get descent of this font which is the distance from ** baseline to bottom of chars, not including any leading area. abstract Float descent() ** Get leading of this font which is the distance above ** the ascent which may include accents and other marks. abstract Float leading() ** Get the width of the string when painted with this font. abstract Float width(Str s) } ************************************************************************** ** FontDataMetrics ************************************************************************** ** FontDataMetrics implements metrics via internal, predefined FontData @Js internal const class FontDataMetrics : FontMetrics { @NoDoc new make(DeviceContext dc, Float size, FontData data) { this.data = data this.size = size this.ratio = (dc.dpi / 72f * fudge) * size / 1000f } ** Get height of this font which is the sum of ** ascent, descent, and leading. override Float height() { (data.height * ratio).round } ** Get ascent of this font which is the distance from ** baseline to top of chars, not including any leading area. override Float ascent() { (data.ascent * ratio).round } ** Get descent of this font which is the distance from ** baseline to bottom of chars, not including any leading area. override Float descent() { (data.descent * ratio).round } ** Get leading of this font which is the distance above ** the ascent which may include accents and other marks. override Float leading() { (data.leading * ratio).round } ** Get the width of the string when painted with this font. override Float width(Str s) { d := data w := 0 for (i := 0; i<s.size; ++i) w += d.charWidth(s[i]) return (w.toFloat * ratio).round } ** Last char we have metrics for @NoDoc Int lastChar() { data.lastChar } ** This factor is used to tune metrics based on eye-ball testing ** since we have simplified metric data for efficiency private static const Float fudge := 1.02f private const FontData data // backing metric data private const Float size // font size in points private const Float ratio // ratio to map 1000pt to device context } ************************************************************************** ** FontWeight ************************************************************************** ** Font weight property values @Js enum class FontWeight { thin(100), extraLight(200), light(300), normal(400), medium(500), semiBold(600), bold(700), extraBold(800), black(900) ** Numeric weight as number from 100 to 900 const Int num ** Is this the normal value Bool isNormal() { this === normal } ** From numeric value 100 to 900 static FontWeight? fromNum(Int num, Bool checked := true) { switch (num) { case 100: return thin case 200: return extraLight case 300: return light case 400: return normal case 500: return medium case 600: return semiBold case 700: return bold case 800: return extraBold case 900: return black } if (checked) throw ArgErr("Invalid FontWeight num: $num") return null } ** Decode from CSS string @NoDoc static FontWeight? decode(Str s, Bool checked := true) { try { val := fromStr(s, false) if (val != null) return val return fromNum(s.toInt) } catch (Err e) {} if (checked) throw ArgErr("Invalid FontWeight: $s") return null } private new make(Int num) { this.num = num } } ************************************************************************** ** FontStyle ************************************************************************** ** Font style property values: normal, italic, oblique @Js enum class FontStyle { normal, italic, oblique ** Is this the normal value Bool isNormal() { this === normal } ** Decode from CSS string @NoDoc static FontStyle? decode(Str s, Bool checked := true) { fromStr(s, checked) } }