v0.6.1 aa12db3

edit_preedit.go RawHistoryFolder

1package ui
2
3import (
4	"math"
5	"unicode/utf8"
6
7	"github.com/bnema/nefergui/internal/css"
8	"github.com/bnema/nefergui/internal/edit"
9	"github.com/bnema/nefergui/internal/layout"
10	"github.com/bnema/nefergui/internal/text"
11)
12
13// shapeAt shapes s in the element's font and letter spacing, as layout
14// measures editor text, and places its runs from the logical point (x, y).
15// wrap > 0 breaks lines at that width. box > 0 aligns each line within a box
16// of that width by text-align; zero keeps lines starting at x.
17func shapeAt(engine *text.Engine, st *css.Style, s string, x, y, wrap, box float64) (text.Layout, []layout.Run, bool) {
18	req := text.Request{Families: st.FontFamily, Weight: float32(st.FontWeight), Italic: st.FontStyle == css.KeywordItalic || st.FontStyle == css.KeywordOblique, Size: st.FontSize.Value}
19	if st.LetterSpacing.Unit == "px" { // percentages resolve to 0, as in layout
20		req.LetterSpacing = st.LetterSpacing.Value
21	}
22	if req.Size <= 0 {
23		req.Size = 16
24	}
25	measured, err := engine.Measure(s, req, wrap)
26	if err != nil || len(measured.Lines) == 0 {
27		return text.Layout{}, nil, false
28	}
29	var runs []layout.Run
30	for _, line := range measured.Lines {
31		lx := x
32		if box > 0 {
33			lx += layout.AlignOffset(st.TextAlign, line.Direction, box, line.Width)
34		}
35		for _, run := range line.Runs {
36			rr := layout.Run{Start: run.Start, End: run.End, X: lx + run.X, Y: y + run.Y, Advance: run.Advance, Face: run.Face, FaceID: run.Face.ID, Size: req.Size}
37			for _, g := range run.Glyphs {
38				rr.Glyphs = append(rr.Glyphs, layout.Glyph{ID: uint32(g.ID), X: lx + g.X, Y: y + g.Y, Advance: g.Advance, Cluster: g.Cluster})
39			}
40			runs = append(runs, rr)
41		}
42	}
43	return measured, runs, true
44}
45
46// placeholderCommand paints the placeholder of an empty editor in its content
47// box, aligned like editor text and dimmed to half the text opacity. Textarea
48// placeholders wrap at the content width. It never enters the bound value.
49func placeholderCommand(e *element, n *layout.Result, engine *text.Engine) []layout.Command {
50	if e.computed == nil {
51		return nil
52	}
53	st := e.computed.Style
54	x, y := n.Content.X-n.ScrollX, n.Content.Y-n.ScrollY
55	wrap := 0.0
56	if e.typ == "textarea" {
57		wrap = n.Content.W
58	}
59	measured, runs, ok := shapeAt(engine, st, e.placeholder, x, y, wrap, n.Content.W)
60	if !ok {
61		return nil
62	}
63	opacity := math.Min(1, st.Opacity) * 0.5
64	return []layout.Command{{Op: "text", ID: n.ID + "#placeholder", Rect: layout.Rect{X: x, Y: y, W: n.Content.W, H: measured.Height}, Text: e.placeholder, Runs: runs, Color: st.Color, Opacity: opacity}}
65}
66
67// preeditCommands shapes the composition independently of the bound value.
68// Its text command begins at the model caret and carries its own rune clusters;
69// the underline and composition cursor refer to UTF-8 byte positions.
70func preeditCommands(s *edit.State, n *layout.Result, e *element, engine *text.Engine, display []layout.Command) []layout.Command {
71	if s.Preedit == "" || n == nil || e.computed == nil {
72		return nil
73	}
74	// The composition starts at the same caret position that is painted.
75	geometry := newEditorGeometry(s, n, display, e.password)
76	origin, found := geometry.caret(s.Cursor)
77	x, y := n.Content.X, n.Content.Y
78	if found {
79		x, y = origin.x, origin.y
80	}
81	st := e.computed.Style
82	preedit := s.Preedit
83	if e.password {
84		preedit = (&edit.State{Value: s.Preedit}).Mask()
85	}
86	measured, runs, ok := shapeAt(engine, st, preedit, x, y, 0, 0)
87	if !ok {
88		return nil
89	}
90	h := measured.LineHeight
91	if h <= 0 {
92		h = n.Content.H
93	}
94	width := measured.Width
95	color := st.Color
96	cmds := []layout.Command{{Op: "preedit-text", ID: n.ID, Rect: layout.Rect{X: x, Y: y, W: width, H: h}, Text: preedit, Runs: runs, Color: color, Opacity: 1}, {Op: "preedit-underline", ID: n.ID, Rect: layout.Rect{X: x, Y: y + h - 1, W: width, H: 1}, Color: color, Opacity: 1}}
97	pos := s.PreeditBegin
98	if pos < 0 {
99		pos = 0
100	}
101	if pos > len(s.Preedit) {
102		pos = len(s.Preedit)
103	}
104	clamped := 0
105	for _, b := range edit.Boundaries(s.Preedit) {
106		if b > pos {
107			break
108		}
109		clamped = b
110	}
111	pos = clamped
112	// Preedit offsets are bytes in the original text; a password shows one
113	// bullet per original grapheme, including combined clusters.
114	pos = newDisplayText(s.Preedit, e.password).toShown(pos)
115	// The preedit cursor uses shaped cluster advances, not raw rune count.
116	px := x
117	pr := utf8.RuneCountInString(preedit[:pos])
118	for _, run := range measured.Lines[0].Runs {
119		for _, g := range run.Glyphs {
120			if g.Cluster < pr {
121				px = math.Max(px, x+g.X+g.Advance)
122			}
123		}
124	}
125	cmds = append(cmds, layout.Command{Op: "preedit-caret", ID: n.ID, Rect: layout.Rect{X: px, Y: y, W: 1, H: h}, Color: color, Opacity: 1})
126	return cmds
127}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close