From 5790597a15d001dafec898768ad434890f61443e Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sat, 2 Dec 2023 15:38:16 +0900 Subject: [PATCH] text/v2: add LimitedFace Closes #2857 --- examples/mixedfont/main.go | 31 +++++----- text/v2/limited.go | 112 +++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 13 deletions(-) create mode 100644 text/v2/limited.go diff --git a/examples/mixedfont/main.go b/examples/mixedfont/main.go index 2d9b7aa96..f50ce6c52 100644 --- a/examples/mixedfont/main.go +++ b/examples/mixedfont/main.go @@ -57,19 +57,24 @@ type Game struct { func (g *Game) Update() error { if g.face == nil { - g.face = text.NewMultiFace([]text.Face{ - // goregular.TTF is used primarily. If a glyph is not found in this font, the second font is used. - &text.GoTextFace{ - Source: goRegularFaceSource, - Size: 24, - }, - // M+ Font is the second font. - // Use a relatively big size to see different-sized faces are well mixed. - &text.GoTextFace{ - Source: mplusFaceSource, - Size: 32, - }, + // goregular.TTF is used primarily. If a glyph is not found in this font, the second font is used. + // Use text.LimitedFace to limit the glyphs. + en := text.NewLimitedFace(&text.GoTextFace{ + Source: goRegularFaceSource, + Size: 24, }) + // Limit the glyphs for ASCII and Latin-1 characters for this face. + // This means that, for example, '…' (U+2026) is not rendered by this face. + en.AddUnicodeRange('\u0020', '\u00ff') + + // M+ Font is the second font. + // Use a relatively big size to see different-sized faces are well mixed. + ja := &text.GoTextFace{ + Source: mplusFaceSource, + Size: 32, + } + + g.face = text.NewMultiFace([]text.Face{en, ja}) } return nil } @@ -78,7 +83,7 @@ func (g *Game) Draw(screen *ebiten.Image) { op := &text.DrawOptions{} op.GeoM.Translate(20, 20) op.LineSpacingInPixels = 48 - text.Draw(screen, "HelloこんにちはWorld世界\n日本語とEnglish", g.face, op) + text.Draw(screen, "HelloこんにちはWorld世界\n日本語とEnglish…", g.face, op) } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { diff --git a/text/v2/limited.go b/text/v2/limited.go new file mode 100644 index 000000000..cefeb9c27 --- /dev/null +++ b/text/v2/limited.go @@ -0,0 +1,112 @@ +// Copyright 2023 The Ebitengine Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package text + +import ( + "github.com/hajimehoshi/ebiten/v2/vector" +) + +var _ Face = (*LimitedFace)(nil) + +type LimitedFace struct { + face Face + unicodeRanges unicodeRanges +} + +// NewLimitedFace creates a new LimitedFace from the given face. +// In the default state, glyphs for any runes are limited and not rendered. +// You have to call AddUnicodeRange to add allowed glyphs. +func NewLimitedFace(face Face) *LimitedFace { + return &LimitedFace{ + face: face, + } +} + +// AddUnicodeRange adds a rune range for rendered glyphs. +// A range is inclusive, which means that a range contains the specified rune end. +func (l *LimitedFace) AddUnicodeRange(start, end rune) { + l.unicodeRanges.add(start, end) +} + +// Metrics implements Face. +func (l *LimitedFace) Metrics() Metrics { + return l.face.Metrics() +} + +// advance implements Face. +func (l *LimitedFace) advance(text string) float64 { + return l.face.advance(l.unicodeRanges.filter(text)) +} + +// hasGlyph implements Face. +func (l *LimitedFace) hasGlyph(r rune) bool { + return l.unicodeRanges.contains(r) && l.face.hasGlyph(r) +} + +// appendGlyphsForLine implements Face. +func (l *LimitedFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph { + return l.face.appendGlyphsForLine(glyphs, l.unicodeRanges.filter(line), indexOffset, originX, originY) +} + +// appendVectorPathForLine implements Face. +func (l *LimitedFace) appendVectorPathForLine(path *vector.Path, line string, originX, originY float64) { + l.face.appendVectorPathForLine(path, l.unicodeRanges.filter(line), originX, originY) +} + +// direction implements Face. +func (l *LimitedFace) direction() Direction { + return l.face.direction() +} + +// private implements Face. +func (l *LimitedFace) private() { +} + +type unicodeRange struct { + start rune + end rune +} + +type unicodeRanges struct { + ranges []unicodeRange +} + +func (u *unicodeRanges) add(start, end rune) { + u.ranges = append(u.ranges, unicodeRange{ + start: start, + end: end, + }) +} + +func (u *unicodeRanges) contains(r rune) bool { + for _, rg := range u.ranges { + if rg.start <= r && r <= rg.end { + return true + } + } + return false +} + +func (u *unicodeRanges) filter(str string) string { + var rs []rune + for _, r := range str { + if !u.contains(r) { + // U+FFFD is "REPLACEMENT CHARACTER". + r = '\ufffd' + } + rs = append(rs, r) + } + return string(rs) +}