Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ require (
github.com/go-webengine/engine v0.3.2
github.com/go-widgets/mvvm v0.5.0
github.com/go-widgets/painter v0.2.0
github.com/go-widgets/toolkit v0.124.0
github.com/go-widgets/toolkit v0.127.0
github.com/go-widgets/tray v0.1.1
github.com/jezek/xgb v1.3.1
golang.org/x/image v0.44.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ github.com/go-widgets/painter v0.2.0 h1:C7anwlKLYnlTeCK90xKYvy0srxYXqhuvyDFismlf
github.com/go-widgets/painter v0.2.0/go.mod h1:ccmlkH2UmcXQh6rt9Fu2eDt2RI0B5V0POaGmUKeW0EQ=
github.com/go-widgets/toolkit v0.124.0 h1:o+DOb6jZdEuq2y4yWNLKwXSxlbdPaFjBYO918HAXPCc=
github.com/go-widgets/toolkit v0.124.0/go.mod h1:RfJFcIy5JU/klT40+hC1U6qB5DCOh3W92mi7pBvnfe0=
github.com/go-widgets/toolkit v0.127.0 h1:8ldvVeRiOQOBvTQXoSu1w8CmEnYLOlU3ZTK/gtqwQBw=
github.com/go-widgets/toolkit v0.127.0/go.mod h1:hekxU+C0moasoJPAI366VWNiZBbRSAjloC0tKnwXkWY=
github.com/go-widgets/tray v0.1.1 h1:6wpYFpfkObLbxmrI8IyqcP528Ml+OrVT+msRjSpncDg=
github.com/go-widgets/tray v0.1.1/go.mod h1:qftThNTUbYYsk/oWCjDvwDNRIojtYhY6zRZLP6c001A=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
Expand Down
18 changes: 10 additions & 8 deletions ui/detail.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,18 @@ func (s *Scene) detailContent() detailBody {
titleFace := getFace(rpxOf(s, 22), true)
bodyFace := getFace(rpxOf(s, 15), false)
it := s.detail
// Wrap with the SAME fonts drawDetail draws these lines with (stock toolkit
// Labels carrying ttFont), not with the textFace whose heights size the rows.
// wrapMeasured says so explicitly, and the feed card already obeys it: a line
// that "fits" one rasteriser can overflow the other and be clipped mid-word.
// Composed emoji widened the gap β€” a ZWJ sequence is two glyphs to the
// x/image measurer and one to the shaper that actually draws it.
titleWrap := ttFont(true, rpxOf(s, 22))
bodyWrap := ttFont(false, rpxOf(s, 15))
d := detailBody{
x: x, w: w, titleFace: titleFace, bodyFace: bodyFace,
titleLines: wrapText(titleFace, it.Title, w),
bodyLines: wrapText(bodyFace, stripHTML(it.Body), w),
titleLines: wrapMeasured(titleWrap.Measure, it.Title, w),
bodyLines: wrapMeasured(bodyWrap.Measure, stripHTML(it.Body), w),
meta: metaLine(it),
}
gap := rpxOf(s, 10)
Expand Down Expand Up @@ -180,12 +188,6 @@ func (s *Scene) detailHitTest(x, y int) Hit {
return Hit{Kind: HitNone}
}

// wrapText greedily word-wraps text to maxW pixels in face, preserving paragraph
// breaks ("\n"). A word longer than maxW is left un-broken on its own line.
func wrapText(face textFace, text string, maxW int) []string {
return wrapMeasured(face.width, text, maxW)
}

// wrapMeasured is the width-measurer-agnostic core of wrapText: it greedily
// word-wraps text to maxW pixels, deciding each break with measure(s). Callers
// whose text is rendered by a toolkit.Font (not a textFace) must pass that
Expand Down
17 changes: 11 additions & 6 deletions ui/detail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,22 +116,27 @@ func TestDetailScroll(t *testing.T) {
}

func TestWrapText(t *testing.T) {
f := getFace(14, false)
if wrapText(f, " ", 100) != nil {
// wrapMeasured is the only wrapper now: wrapText, which defaulted to the
// textFace measurer, was removed because every caller draws with a
// toolkit.Font and must wrap against THAT font's metrics.
wrap := func(text string, maxW int) []string {
return wrapMeasured(getFace(14, false).width, text, maxW)
}
if wrap(" ", 100) != nil {
t.Fatal("blank -> nil")
}
if got := wrapText(f, "hi", 10000); len(got) != 1 || got[0] != "hi" {
if got := wrap("hi", 10000); len(got) != 1 || got[0] != "hi" {
t.Fatalf("fits = %v", got)
}
if got := wrapText(f, "one two three four five six seven eight nine ten", 60); len(got) < 2 {
if got := wrap("one two three four five six seven eight nine ten", 60); len(got) < 2 {
t.Fatalf("should wrap: %v", got)
}
// A single word wider than maxW stays on its own line.
if got := wrapText(f, "supercalifragilisticexpialidocious", 20); len(got) != 1 {
if got := wrap("supercalifragilisticexpialidocious", 20); len(got) != 1 {
t.Fatalf("long word = %v", got)
}
// Paragraph break preserved as a blank line.
got := wrapText(f, "a\n\nb", 1000)
got := wrap("a\n\nb", 1000)
if len(got) != 3 || got[1] != "" {
t.Fatalf("paragraphs = %v", got)
}
Expand Down
33 changes: 33 additions & 0 deletions ui/emoji_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,39 @@ func TestEmojiRenderInTheToolkitChain(t *testing.T) {
}
}

// TestZWJSequencesComposeToOneGlyph is the payoff of routing whole graphemes to
// one face (toolkit v0.127.0): the sequence reaches the shaper intact, so the
// GSUB ligature fires and πŸ§‘β€πŸš€ is ONE astronaut rather than a person standing
// next to a rocket. Before, the joiner was claimed by the Thai face β€” which
// ships U+200D for its own shaping and sits earlier in the chain β€” and the
// sequence arrived as three separate runs.
func TestZWJSequencesComposeToOneGlyph(t *testing.T) {
f := ttFont(false, 32)
const (
person = "\U0001F9D1"
rocket = "\U0001F680"
zwj = "‍"
)
one := f.Measure(person)
if one <= 0 {
t.Fatal("the emoji face measured nothing")
}
if got := f.Measure(person + zwj + rocket); got != one {
t.Fatalf("πŸ§‘β€πŸš€ measures %d, want one glyph's %d (un-composed would be %d)",
got, one, f.Measure(person+rocket))
}
// A three-part family sequence composes too.
family := "\U0001F468" + zwj + "\U0001F469" + zwj + "\U0001F466"
if got := f.Measure(family); got != one {
t.Fatalf("πŸ‘¨β€πŸ‘©β€πŸ‘¦ measures %d, want one glyph's %d", got, one)
}
// The card wraps with this very measurer, so a composed sequence costs one
// glyph of line width rather than two.
if f.Measure("a"+person+zwj+rocket+"b") != f.Measure("a"+person+"b") {
t.Fatal("a sequence embedded in text did not compose")
}
}

// TestFormatCharactersAreNeverRasterised covers the defect that only becomes
// visible once emoji render: an emoji sequence is held together by invisible
// controls (πŸ§‘β€πŸš€ is person + ZWJ + rocket), and nothing in the chain has a glyph
Expand Down
18 changes: 9 additions & 9 deletions ui/icons.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ func iconInset(r toolkit.Rect, frac int) toolkit.Rect {
// Cached Iconoir icon lookups. Get is cheap, but caching avoids a registry
// lookup per frame. Names verified present in iconoir v0.1.0.
var (
iconMenu = iconoir.MustGet("menu") // burger / open-sidebar
iconLock = iconoir.MustGet("lock") // auth-banner padlock
iconUser = iconoir.MustGet("user") // sidebar Accounts
iconList = iconoir.MustGet("list") // sidebar Network log
iconSettings = iconoir.MustGet("settings") // sidebar Settings (gear)
iconSearch = iconoir.MustGet("search") // topbar SearchEntry magnifier
iconPlus = iconoir.MustGet("plus") // sidebar "Browse newsgroups" + subscribe
iconRefresh = iconoir.MustGet("refresh-double") // browse view Refresh control + web preview Reload
iconCheck = iconoir.MustGet("check") // subscribed / complete marker
iconMenu = iconoir.MustGet("menu") // burger / open-sidebar
iconLock = iconoir.MustGet("lock") // auth-banner padlock
iconUser = iconoir.MustGet("user") // sidebar Accounts
iconList = iconoir.MustGet("list") // sidebar Network log
iconSettings = iconoir.MustGet("settings") // sidebar Settings (gear)
iconSearch = iconoir.MustGet("search") // topbar SearchEntry magnifier
iconPlus = iconoir.MustGet("plus") // sidebar "Browse newsgroups" + subscribe
iconRefresh = iconoir.MustGet("refresh-double") // browse view Refresh control + web preview Reload
iconCheck = iconoir.MustGet("check") // subscribed / complete marker
iconNavLeft = iconoir.MustGet("nav-arrow-left") // web preview Back
iconNavRight = iconoir.MustGet("nav-arrow-right") // web preview Forward
iconZoomIn = iconoir.MustGet("zoom-in") // web preview zoom in
Expand Down
9 changes: 6 additions & 3 deletions ui/preview_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,10 +370,13 @@ func (s *Scene) previewContent() previewBody {
titleFace := getFace(rpxOf(s, 17), true)
bodyFace := getFace(rpxOf(s, 14), false)
it := s.previewItem
// Wrap with the SAME fonts drawPreview draws these lines with (stock toolkit
// Labels carrying ttFont), not with the textFaces whose heights size the
// rows β€” see wrapMeasured.
d := previewBody{
innerX: x, innerW: w, titleFace: titleFace, bodyFace: bodyFace,
titleLines: wrapText(titleFace, it.Title, w),
bodyLines: wrapText(bodyFace, stripHTML(it.Body), w),
titleLines: wrapMeasured(ttFont(true, rpxOf(s, 17)).Measure, it.Title, w),
bodyLines: wrapMeasured(ttFont(false, rpxOf(s, 14)).Measure, stripHTML(it.Body), w),
meta: metaLine(it),
}
gap := rpxOf(s, 8)
Expand Down Expand Up @@ -467,7 +470,7 @@ func (s *Scene) layoutPreview() {
// fixed web-preview header.
func (s *Scene) previewHeaderLines() []string {
_, w := s.previewInner()
lines := wrapText(getFace(rpxOf(s, 17), true), s.previewItem.Title, w)
lines := wrapMeasured(ttFont(true, rpxOf(s, 17)).Measure, s.previewItem.Title, w)
if len(lines) > 2 {
lines = lines[:2]
}
Expand Down
10 changes: 5 additions & 5 deletions ui/scene.go
Original file line number Diff line number Diff line change
Expand Up @@ -770,11 +770,11 @@ func (s *Scene) Settings() *settings.Settings {
// stored as false and is not re-defaulted to single-tab on the next load.
singleTab := s.BrowserSingleTab()
return &settings.Settings{
Profiles: s.Profiles,
Active: s.activeProf,
Theme: s.themeName,
CachePath: s.cachePath,
Accounts: s.EditedAccounts(),
Profiles: s.Profiles,
Active: s.activeProf,
Theme: s.themeName,
CachePath: s.cachePath,
Accounts: s.EditedAccounts(),
BrowserSingleTab: &singleTab,
HideBrowserChrome: s.BrowserChromeHidden(),
ZoomInKey: s.BrowserZoomInKey(),
Expand Down
Loading