Skip to content

Commit d8f4872

Browse files
hajimehoshiclaude
andcommitted
basicwidget: add ranged font size scales
Consume the last ranged style property: a run's scale override multiplies the base font size for the run's face, flowing through the existing face-run pipeline so wrapping, caret geometry, and hit-testing pick up the scaled advances. The line height stays uniform — scaled runs render on the line's shared baseline, and glyphs scaled past the line height may overlap adjacent lines, matching how browsers lay out mixed font sizes under a fixed line-height. The scale counts as a metric-affecting property, and the core and basicwidget.Text expose SetScaleInRange and UnsetScaleInRange. Styled drawing now aligns each segment's baseline with the base face's by shifting by the ascent difference, since text.Draw positions text by its top. Previously segments were top-aligned, which was invisible while all mixed faces shared the same ascent but misaligns faces with different metrics, such as scaled runs or family overrides. The gallery's Rich Texts sample gains light and scaled ranges, with the sample reworded so the standalone "light" precedes "highlighted", whose inner "light" substring the first-match range helper would style otherwise. Updates #131 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f3e4fc3 commit d8f4872

6 files changed

Lines changed: 56 additions & 3 deletions

File tree

basicwidget/internal/textstyle/statekey.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (r *Runs) HasMetricProperties() bool {
5757
// HasMetricProperties reports whether the style overrides a metric-affecting
5858
// property, such as a variation setting or the font family.
5959
func (s Style) HasMetricProperties() bool {
60-
return len(s.variations) > 0 || len(s.features) > 0 || s.italic.set || s.family.set || s.lang.set
60+
return len(s.variations) > 0 || len(s.features) > 0 || s.italic.set || s.family.set || s.lang.set || s.scale.set
6161
}
6262

6363
// writeStateKey writes the style's consumed properties into w.
@@ -95,6 +95,11 @@ func (s Style) writeMetricProperties(w Writer) {
9595
italic, ok := s.Italic()
9696
w.WriteBool(ok)
9797
w.WriteBool(italic)
98+
scale, ok := s.Scale()
99+
w.WriteBool(ok)
100+
if ok {
101+
w.WriteFloat64(scale)
102+
}
98103
lang, ok := s.Lang()
99104
w.WriteBool(ok)
100105
if ok {

basicwidget/internal/textstyle/style_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ func TestStyleHasMetricProperties(t *testing.T) {
6060
style: textstyle.Style{}.WithLang(language.Japanese),
6161
want: true,
6262
},
63+
{
64+
name: "scale",
65+
style: textstyle.Style{}.WithScale(1.5),
66+
want: true,
67+
},
6368
}
6469

6570
for _, tt := range tests {

basicwidget/internal/textutil/draw.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ func faceRunsIntersect(faceRuns []FaceRun, start, end int) bool {
460460
func drawStyledVisualLine(dst *ebiten.Image, vlStr string, lineStart, contentEnd int, runs []StyleRun, options *DrawOptions, op *text.DrawOptions) {
461461
origGeoM := op.GeoM
462462
origColorScale := op.ColorScale
463+
baseAscent := options.Face.TextFace().Metrics().HAscent
463464
var x float64
464465
var i int
465466
for i < len(vlStr) {
@@ -482,12 +483,16 @@ func drawStyledVisualLine(dst *ebiten.Image, vlStr string, lineStart, contentEnd
482483
}
483484
seg := vlStr[i:segEnd]
484485
face := segFace.TextFace()
486+
// text.Draw positions text by its top, so a face whose ascent
487+
// differs from the base face's shifts by the difference to keep the
488+
// baselines aligned.
489+
dy := baseAscent - face.Metrics().HAscent
485490
var chunkStart int
486491
for chunkStart < len(seg) {
487492
clr, colorChange := styleRunColorAt(runs, options.TextColor, contentEnd, lineStart+i+chunkStart)
488493
chunkEnd := min(colorChange-lineStart-i, len(seg))
489494
geoM := op.GeoM
490-
op.GeoM.Translate(x+text.AdvanceAt(seg, chunkStart, face), 0)
495+
op.GeoM.Translate(x+text.AdvanceAt(seg, chunkStart, face), dy)
491496
op.ColorScale.Reset()
492497
op.ColorScale.ScaleWithColor(clr)
493498
text.Draw(dst, seg[chunkStart:chunkEnd], face, op)

basicwidget/internal/textwidget/style.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,20 @@ func (t *Text) UnsetFontFamilyInRange(startInBytes, endInBytes int) {
224224
t.ensureStyleRuns().UnsetFamily(startInBytes, endInBytes)
225225
}
226226

227+
// SetScaleInRange overrides the font size in [startInBytes, endInBytes) as a
228+
// multiplier applied to the base font size. The override lasts until the
229+
// value changes. The line height is unaffected; the range renders on the
230+
// line's baseline.
231+
func (t *Text) SetScaleInRange(startInBytes, endInBytes int, scale float64) {
232+
t.ensureStyleRuns().SetScale(startInBytes, endInBytes, scale)
233+
}
234+
235+
// UnsetScaleInRange removes the font size override in
236+
// [startInBytes, endInBytes).
237+
func (t *Text) UnsetScaleInRange(startInBytes, endInBytes int) {
238+
t.ensureStyleRuns().UnsetScale(startInBytes, endInBytes)
239+
}
240+
227241
// SetLangInRange overrides the language used to select the face and its
228242
// features when shaping [startInBytes, endInBytes). The override lasts until
229243
// the value changes.
@@ -340,6 +354,9 @@ func (t *Text) appendFaceRunsForStyle(runs []textutil.FaceRun, context *guigui.C
340354
if italic, ok := run.Style.Italic(); ok {
341355
attrs.Italic = italic
342356
}
357+
if scale, ok := run.Style.Scale(); ok {
358+
attrs.Size *= scale
359+
}
343360
if lang, ok := run.Style.Lang(); ok {
344361
attrs.Lang = lang
345362
}

basicwidget/text.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,21 @@ func (t *Text) UnsetItalicInRange(startInBytes, endInBytes int) {
447447
t.core.UnsetItalicInRange(startInBytes, endInBytes)
448448
}
449449

450+
// SetScaleInRange overrides the font size in [startInBytes, endInBytes) as a
451+
// multiplier applied to the base font size. The override lasts until the
452+
// value changes. The line height is unaffected; the range renders on the
453+
// line's baseline, and glyphs scaled past the line height may overlap
454+
// adjacent lines.
455+
func (t *Text) SetScaleInRange(startInBytes, endInBytes int, scale float64) {
456+
t.core.SetScaleInRange(startInBytes, endInBytes, scale)
457+
}
458+
459+
// UnsetScaleInRange removes the font size override in
460+
// [startInBytes, endInBytes).
461+
func (t *Text) UnsetScaleInRange(startInBytes, endInBytes int) {
462+
t.core.UnsetScaleInRange(startInBytes, endInBytes)
463+
}
464+
450465
// SetLangInRange overrides the language used to select the face and its
451466
// features when shaping [startInBytes, endInBytes). The override lasts until
452467
// the value changes.

example/gallery/richtexts.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"github.com/guigui-gui/guigui/basicwidget"
1515
)
1616

17-
const richTextsSampleText = "Colored, highlighted, underlined, struck-through, and bold ranges.\n" +
17+
const richTextsSampleText = "Colored, light, highlighted, underlined, struck-through, bold, and scaled ranges.\n" +
1818
"Combined styles apply to a single range.\n" +
1919
"A sufficiently long styled range continues across the visual line boundary when the text wraps, keeping its background and underline on every visual line it covers.\n" +
2020
"日本語のテキストにも下線と背景を適用できます。"
@@ -86,6 +86,12 @@ func (r *RichTexts) Build(context *guigui.Context, adder *guigui.ChildAdder) err
8686
styleRichTextsSampleRange("bold", func(start, end int) {
8787
t.SetWeightInRange(start, end, text.WeightBold)
8888
})
89+
styleRichTextsSampleRange("light", func(start, end int) {
90+
t.SetWeightInRange(start, end, text.WeightLight)
91+
})
92+
styleRichTextsSampleRange("scaled", func(start, end int) {
93+
t.SetScaleInRange(start, end, 1.5)
94+
})
8995
styleRichTextsSampleRange("Combined styles", func(start, end int) {
9096
t.SetColorInRange(start, end, blue)
9197
t.SetBackgroundColorInRange(start, end, green)

0 commit comments

Comments
 (0)