mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
go.*,text,widget{,/material}: enable configurable line wrapping within words
This commit enables consumers of the text shaper to select a policy for how line breaking candidates will be chosen. The new default policy can break lines within "words" (UAX#14 segments) when words do not fit by themselves on a line. This ensures that text does not horizontally overflow its bounding box unless the available width is insufficient to display a single UAX#29 grapheme cluster. Fixes: https://todo.sr.ht/~eliasnaur/gio/467 Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
@@ -6,7 +6,7 @@ require (
|
|||||||
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
|
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
|
||||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
|
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
|
||||||
gioui.org/shader v1.0.6
|
gioui.org/shader v1.0.6
|
||||||
github.com/go-text/typesetting v0.0.0-20230413204129-b4f0492bf7ae
|
github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433
|
||||||
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
|
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
|
||||||
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
|
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
|
||||||
golang.org/x/image v0.5.0
|
golang.org/x/image v0.5.0
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJG
|
|||||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||||
gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y=
|
gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y=
|
||||||
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
||||||
github.com/go-text/typesetting v0.0.0-20230413204129-b4f0492bf7ae h1:LCcaQgYrnS+sx9Tc3oGUvbRBRt+5oFnKWakaxeAvNVI=
|
github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433 h1:Pdyvqsfi1QYgFfZa4R8otBOtgO+CGyBDMEG8cM3jwvE=
|
||||||
github.com/go-text/typesetting v0.0.0-20230413204129-b4f0492bf7ae/go.mod h1:KmrpWuSMFcO2yjmyhGpnBGQHSKAoEgMTSSzvLDzCuEA=
|
github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433/go.mod h1:KmrpWuSMFcO2yjmyhGpnBGQHSKAoEgMTSSzvLDzCuEA=
|
||||||
github.com/go-text/typesetting-utils v0.0.0-20230412163830-89e4bcfa3ecc h1:9Kf84pnrmmjdRzZIkomfjowmGUhHs20jkrWYw/I6CYc=
|
github.com/go-text/typesetting-utils v0.0.0-20230412163830-89e4bcfa3ecc h1:9Kf84pnrmmjdRzZIkomfjowmGUhHs20jkrWYw/I6CYc=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
|||||||
+13
-1
@@ -401,11 +401,23 @@ func (s *shaperImpl) shapeText(faces []font.Face, ppem fixed.Int26_6, lc system.
|
|||||||
return s.outScratchBuf
|
return s.outScratchBuf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wrapPolicyToGoText(p WrapPolicy) shaping.LineBreakPolicy {
|
||||||
|
switch p {
|
||||||
|
case WrapGraphemes:
|
||||||
|
return shaping.Always
|
||||||
|
case WrapWords:
|
||||||
|
return shaping.Never
|
||||||
|
default:
|
||||||
|
return shaping.WhenNecessary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// shapeAndWrapText invokes the text shaper and returns wrapped lines in the shaper's native format.
|
// shapeAndWrapText invokes the text shaper and returns wrapped lines in the shaper's native format.
|
||||||
func (s *shaperImpl) shapeAndWrapText(faces []font.Face, params Parameters, txt []rune) (_ []shaping.Line, truncated int) {
|
func (s *shaperImpl) shapeAndWrapText(faces []font.Face, params Parameters, txt []rune) (_ []shaping.Line, truncated int) {
|
||||||
wc := shaping.WrapConfig{
|
wc := shaping.WrapConfig{
|
||||||
TruncateAfterLines: params.MaxLines,
|
TruncateAfterLines: params.MaxLines,
|
||||||
TextContinues: params.forceTruncate,
|
TextContinues: params.forceTruncate,
|
||||||
|
BreakPolicy: wrapPolicyToGoText(params.WrapPolicy),
|
||||||
}
|
}
|
||||||
if wc.TruncateAfterLines > 0 {
|
if wc.TruncateAfterLines > 0 {
|
||||||
if len(params.Truncator) == 0 {
|
if len(params.Truncator) == 0 {
|
||||||
@@ -416,7 +428,7 @@ func (s *shaperImpl) shapeAndWrapText(faces []font.Face, params Parameters, txt
|
|||||||
wc.Truncator = s.shapeText(faces, params.PxPerEm, params.Locale, []rune(params.Truncator))[0]
|
wc.Truncator = s.shapeText(faces, params.PxPerEm, params.Locale, []rune(params.Truncator))[0]
|
||||||
}
|
}
|
||||||
// Wrap outputs into lines.
|
// Wrap outputs into lines.
|
||||||
return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, s.shapeText(faces, params.PxPerEm, params.Locale, txt)...)
|
return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, shaping.NewSliceIterator(s.shapeText(faces, params.PxPerEm, params.Locale, txt)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// replaceControlCharacters replaces problematic unicode
|
// replaceControlCharacters replaces problematic unicode
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
nsareg "eliasnaur.com/font/noto/sans/arabic/regular"
|
nsareg "eliasnaur.com/font/noto/sans/arabic/regular"
|
||||||
"github.com/go-text/typesetting/font"
|
"github.com/go-text/typesetting/font"
|
||||||
"github.com/go-text/typesetting/shaping"
|
"github.com/go-text/typesetting/shaping"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
"golang.org/x/image/font/gofont/goregular"
|
"golang.org/x/image/font/gofont/goregular"
|
||||||
"golang.org/x/image/math/fixed"
|
"golang.org/x/image/math/fixed"
|
||||||
|
|
||||||
@@ -235,6 +236,21 @@ func complexGlyph(cluster, runes, glyphs int) shaping.Glyph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copyLines performs a deep copy of the provided lines. This is necessary if you
|
||||||
|
// want to use the line wrapper again while also using the lines.
|
||||||
|
func copyLines(lines []shaping.Line) []shaping.Line {
|
||||||
|
out := make([]shaping.Line, len(lines))
|
||||||
|
for lineIdx, line := range lines {
|
||||||
|
lineCopy := make([]shaping.Output, len(line))
|
||||||
|
for runIdx, run := range line {
|
||||||
|
lineCopy[runIdx] = run
|
||||||
|
lineCopy[runIdx].Glyphs = slices.Clone(run.Glyphs)
|
||||||
|
}
|
||||||
|
out[lineIdx] = lineCopy
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// makeTestText creates a simple and complex(bidi) sample of shaped text at the given
|
// makeTestText creates a simple and complex(bidi) sample of shaped text at the given
|
||||||
// font size and wrapped to the given line width. The runeLimit, if nonzero,
|
// font size and wrapped to the given line width. The runeLimit, if nonzero,
|
||||||
// truncates the sample text to ensure shorter output for expensive tests.
|
// truncates the sample text to ensure shorter output for expensive tests.
|
||||||
@@ -277,11 +293,13 @@ func makeTestText(shaper *shaperImpl, primaryDir system.TextDirection, fontSize,
|
|||||||
MaxWidth: lineWidth,
|
MaxWidth: lineWidth,
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
}, []rune(simpleSource))
|
}, []rune(simpleSource))
|
||||||
|
simpleText = copyLines(simpleText)
|
||||||
complexText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(giofont.Font{}), Parameters{
|
complexText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(giofont.Font{}), Parameters{
|
||||||
PxPerEm: fixed.I(fontSize),
|
PxPerEm: fixed.I(fontSize),
|
||||||
MaxWidth: lineWidth,
|
MaxWidth: lineWidth,
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
}, []rune(complexSource))
|
}, []rune(complexSource))
|
||||||
|
complexText = copyLines(complexText)
|
||||||
testShaper(rtlFace, ltrFace)
|
testShaper(rtlFace, ltrFace)
|
||||||
return simpleText, complexText
|
return simpleText, complexText
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ type layoutKey struct {
|
|||||||
locale system.Locale
|
locale system.Locale
|
||||||
font giofont.Font
|
font giofont.Font
|
||||||
forceTruncate bool
|
forceTruncate bool
|
||||||
|
wrapPolicy WrapPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
type pathKey struct {
|
type pathKey struct {
|
||||||
|
|||||||
@@ -16,6 +16,27 @@ import (
|
|||||||
"golang.org/x/image/math/fixed"
|
"golang.org/x/image/math/fixed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WrapPolicy configures strategies for choosing where to break lines of text for line
|
||||||
|
// wrapping.
|
||||||
|
type WrapPolicy uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// WrapHeuristically tries to minimize breaking within words (UAX#14 text segments)
|
||||||
|
// while also ensuring that text fits within the given MaxWidth. It will only break
|
||||||
|
// a line within a word (on a UAX#29 grapheme cluster boundary) when that word cannot
|
||||||
|
// fit on a line by itself. Additionally, when the final word of a line is being
|
||||||
|
// truncated, this policy will preserve as many symbols of that word as
|
||||||
|
// possible before the truncator.
|
||||||
|
WrapHeuristically WrapPolicy = iota
|
||||||
|
// WrapWords does not permit words (UAX#14 text segments) to be broken across lines.
|
||||||
|
// This means that sometimes long words will exceed the MaxWidth they are wrapped with.
|
||||||
|
WrapWords
|
||||||
|
// WrapGraphemes will maximize the amount of text on each line at the expense of readability,
|
||||||
|
// breaking any word across lines on UAX#29 grapheme cluster boundaries to maximize the number of
|
||||||
|
// grapheme clusters on each line.
|
||||||
|
WrapGraphemes
|
||||||
|
)
|
||||||
|
|
||||||
// Parameters are static text shaping attributes applied to the entire shaped text.
|
// Parameters are static text shaping attributes applied to the entire shaped text.
|
||||||
type Parameters struct {
|
type Parameters struct {
|
||||||
// Font describes the preferred typeface.
|
// Font describes the preferred typeface.
|
||||||
@@ -32,6 +53,9 @@ type Parameters struct {
|
|||||||
// truncated.
|
// truncated.
|
||||||
Truncator string
|
Truncator string
|
||||||
|
|
||||||
|
// WrapPolicy configures how line breaks will be chosen when wrapping text across lines.
|
||||||
|
WrapPolicy WrapPolicy
|
||||||
|
|
||||||
// MinWidth and MaxWidth provide the minimum and maximum horizontal space constraints
|
// MinWidth and MaxWidth provide the minimum and maximum horizontal space constraints
|
||||||
// for the shaped text.
|
// for the shaped text.
|
||||||
MinWidth, MaxWidth int
|
MinWidth, MaxWidth int
|
||||||
@@ -318,6 +342,7 @@ func (l *Shaper) layoutParagraph(params Parameters, asStr string, asBytes []byte
|
|||||||
locale: params.Locale,
|
locale: params.Locale,
|
||||||
font: params.Font,
|
font: params.Font,
|
||||||
forceTruncate: params.forceTruncate,
|
forceTruncate: params.forceTruncate,
|
||||||
|
wrapPolicy: params.WrapPolicy,
|
||||||
str: asStr,
|
str: asStr,
|
||||||
}
|
}
|
||||||
if l, ok := l.layoutCache.Get(lk); ok {
|
if l, ok := l.layoutCache.Get(lk); ok {
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ type Editor struct {
|
|||||||
// Filter is the list of characters allowed in the Editor. If Filter is empty,
|
// Filter is the list of characters allowed in the Editor. If Filter is empty,
|
||||||
// all characters are allowed.
|
// all characters are allowed.
|
||||||
Filter string
|
Filter string
|
||||||
|
// WrapPolicy configures how displayed text will be broken into lines.
|
||||||
|
WrapPolicy text.WrapPolicy
|
||||||
|
|
||||||
buffer *editBuffer
|
buffer *editBuffer
|
||||||
// scratch is a byte buffer that is reused to efficiently read portions of text
|
// scratch is a byte buffer that is reused to efficiently read portions of text
|
||||||
@@ -504,6 +506,7 @@ func (e *Editor) initBuffer() {
|
|||||||
e.text.Alignment = e.Alignment
|
e.text.Alignment = e.Alignment
|
||||||
e.text.SingleLine = e.SingleLine
|
e.text.SingleLine = e.SingleLine
|
||||||
e.text.Mask = e.Mask
|
e.text.Mask = e.Mask
|
||||||
|
e.text.WrapPolicy = e.WrapPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout lays out the editor using the provided textMaterial as the paint material
|
// Layout lays out the editor using the provided textMaterial as the paint material
|
||||||
|
|||||||
@@ -409,6 +409,7 @@ func TestEditorRTL(t *testing.T) {
|
|||||||
|
|
||||||
func TestEditorLigature(t *testing.T) {
|
func TestEditorLigature(t *testing.T) {
|
||||||
e := new(Editor)
|
e := new(Editor)
|
||||||
|
e.WrapPolicy = text.WrapWords
|
||||||
gtx := layout.Context{
|
gtx := layout.Context{
|
||||||
Ops: new(op.Ops),
|
Ops: new(op.Ops),
|
||||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||||
|
|||||||
+11
-8
@@ -28,6 +28,8 @@ type Label struct {
|
|||||||
// Truncator is the text that will be shown at the end of the final
|
// Truncator is the text that will be shown at the end of the final
|
||||||
// line if MaxLines is exceeded. Defaults to "…" if empty.
|
// line if MaxLines is exceeded. Defaults to "…" if empty.
|
||||||
Truncator string
|
Truncator string
|
||||||
|
// WrapPolicy configures how displayed text will be broken into lines.
|
||||||
|
WrapPolicy text.WrapPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout the label with the given shaper, font, size, text, and material.
|
// Layout the label with the given shaper, font, size, text, and material.
|
||||||
@@ -35,14 +37,15 @@ func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size
|
|||||||
cs := gtx.Constraints
|
cs := gtx.Constraints
|
||||||
textSize := fixed.I(gtx.Sp(size))
|
textSize := fixed.I(gtx.Sp(size))
|
||||||
lt.LayoutString(text.Parameters{
|
lt.LayoutString(text.Parameters{
|
||||||
Font: font,
|
Font: font,
|
||||||
PxPerEm: textSize,
|
PxPerEm: textSize,
|
||||||
MaxLines: l.MaxLines,
|
MaxLines: l.MaxLines,
|
||||||
Truncator: l.Truncator,
|
Truncator: l.Truncator,
|
||||||
Alignment: l.Alignment,
|
Alignment: l.Alignment,
|
||||||
MaxWidth: cs.Max.X,
|
WrapPolicy: l.WrapPolicy,
|
||||||
MinWidth: cs.Min.X,
|
MaxWidth: cs.Max.X,
|
||||||
Locale: gtx.Locale,
|
MinWidth: cs.Min.X,
|
||||||
|
Locale: gtx.Locale,
|
||||||
}, txt)
|
}, txt)
|
||||||
m := op.Record(gtx.Ops)
|
m := op.Record(gtx.Ops)
|
||||||
viewport := image.Rectangle{Max: cs.Max}
|
viewport := image.Rectangle{Max: cs.Max}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ type LabelStyle struct {
|
|||||||
Alignment text.Alignment
|
Alignment text.Alignment
|
||||||
// MaxLines limits the number of lines. Zero means no limit.
|
// MaxLines limits the number of lines. Zero means no limit.
|
||||||
MaxLines int
|
MaxLines int
|
||||||
|
// WrapPolicy configures how displayed text will be broken into lines.
|
||||||
|
WrapPolicy text.WrapPolicy
|
||||||
// Truncator is the text that will be shown at the end of the final
|
// Truncator is the text that will be shown at the end of the final
|
||||||
// line if MaxLines is exceeded. Defaults to "…" if empty.
|
// line if MaxLines is exceeded. Defaults to "…" if empty.
|
||||||
Truncator string
|
Truncator string
|
||||||
@@ -127,12 +129,14 @@ func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
l.State.Alignment = l.Alignment
|
l.State.Alignment = l.Alignment
|
||||||
l.State.MaxLines = l.MaxLines
|
l.State.MaxLines = l.MaxLines
|
||||||
l.State.Truncator = l.Truncator
|
l.State.Truncator = l.Truncator
|
||||||
|
l.State.WrapPolicy = l.WrapPolicy
|
||||||
return l.State.Layout(gtx, l.Shaper, l.Font, l.TextSize, textColor, selectColor)
|
return l.State.Layout(gtx, l.Shaper, l.Font, l.TextSize, textColor, selectColor)
|
||||||
}
|
}
|
||||||
tl := widget.Label{
|
tl := widget.Label{
|
||||||
Alignment: l.Alignment,
|
Alignment: l.Alignment,
|
||||||
MaxLines: l.MaxLines,
|
MaxLines: l.MaxLines,
|
||||||
Truncator: l.Truncator,
|
Truncator: l.Truncator,
|
||||||
|
WrapPolicy: l.WrapPolicy,
|
||||||
}
|
}
|
||||||
return tl.Layout(gtx, l.Shaper, l.Font, l.TextSize, l.Text, textColor)
|
return tl.Layout(gtx, l.Shaper, l.Font, l.TextSize, l.Text, textColor)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ type Selectable struct {
|
|||||||
MaxLines int
|
MaxLines int
|
||||||
// Truncator is the symbol to use at the end of the final line of text
|
// Truncator is the symbol to use at the end of the final line of text
|
||||||
// if text was cut off. Defaults to "…" if left empty.
|
// if text was cut off. Defaults to "…" if left empty.
|
||||||
Truncator string
|
Truncator string
|
||||||
|
// WrapPolicy configures how displayed text will be broken into lines.
|
||||||
|
WrapPolicy text.WrapPolicy
|
||||||
initialized bool
|
initialized bool
|
||||||
source stringSource
|
source stringSource
|
||||||
// scratch is a buffer reused to efficiently read text out of the
|
// scratch is a buffer reused to efficiently read text out of the
|
||||||
@@ -182,6 +184,7 @@ func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font font.Font,
|
|||||||
l.text.Alignment = l.Alignment
|
l.text.Alignment = l.Alignment
|
||||||
l.text.MaxLines = l.MaxLines
|
l.text.MaxLines = l.MaxLines
|
||||||
l.text.Truncator = l.Truncator
|
l.text.Truncator = l.Truncator
|
||||||
|
l.text.WrapPolicy = l.WrapPolicy
|
||||||
l.text.Update(gtx, lt, font, size, l.handleEvents)
|
l.text.Update(gtx, lt, font, size, l.handleEvents)
|
||||||
dims := l.text.Dimensions()
|
dims := l.text.Dimensions()
|
||||||
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
|
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ type textView struct {
|
|||||||
// Truncator is the text that will be shown at the end of the final
|
// Truncator is the text that will be shown at the end of the final
|
||||||
// line if MaxLines is exceeded. Defaults to "…" if empty.
|
// line if MaxLines is exceeded. Defaults to "…" if empty.
|
||||||
Truncator string
|
Truncator string
|
||||||
|
// WrapPolicy configures how displayed text will be broken into lines.
|
||||||
|
WrapPolicy text.WrapPolicy
|
||||||
// Mask replaces the visual display of each rune in the contents with the given rune.
|
// Mask replaces the visual display of each rune in the contents with the given rune.
|
||||||
// Newline characters are not masked. When non-zero, the unmasked contents
|
// Newline characters are not masked. When non-zero, the unmasked contents
|
||||||
// are accessed by Len, Text, and SetText.
|
// are accessed by Len, Text, and SetText.
|
||||||
@@ -267,6 +269,10 @@ func (e *textView) Update(gtx layout.Context, lt *text.Shaper, font font.Font, s
|
|||||||
e.params.MaxLines = e.MaxLines
|
e.params.MaxLines = e.MaxLines
|
||||||
e.invalidate()
|
e.invalidate()
|
||||||
}
|
}
|
||||||
|
if e.WrapPolicy != e.params.WrapPolicy {
|
||||||
|
e.params.WrapPolicy = e.WrapPolicy
|
||||||
|
e.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
e.makeValid()
|
e.makeValid()
|
||||||
if eventHandling != nil {
|
if eventHandling != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user