Implement initial editor mockup with StatusBar, BottomBar, window resize

Elements implemented:
- StatusBar (top bar with filename, action icons, conflict/search icons)
- BottomBar (bottom bar with cursor position, byte position, word wrap button)
- Button (interactive button element)
- AlphaIndex, SearchBar, Cursor, MergeHunk, Toast, Spacer (stubs)

Layout:
- EditorLayout function computes regions for StatusBar and BottomBar
- Filename truncation with ellipsis (mocked filename for testing)
- Mocked data: "very_long_filename...", "Ln 47, Col 12", "1024 / 50000"

Renderer:
- drawStatusBar: draws filename line + icon line
- drawBottomBar: draws cursor pos, byte pos, word wrap button
- drawButton: draws button text
- drawIconButton: draws icon buttons

Window resize:
- configChan setup in main.go
- ConfigEvent handling in event loop
- Layout recomputation on resize

Builds and runs successfully.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Greg Pomerantz 2026-05-08 16:22:46 -04:00
parent 9f36806a4f
commit 869f0b13ac
5 changed files with 466 additions and 40 deletions

View File

@ -2,6 +2,7 @@ package main
import (
"log"
"os"
"gioui.org/app"
"gioui.org/op"
@ -19,6 +20,7 @@ func main() {
if err := run(w); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main()
}
@ -29,29 +31,41 @@ func run(w *app.Window) error {
shaper := text.NewShaper()
renderer := ui.New(ui.Theme{FontSize: 14}, shaper)
elems := starterElements()
// Channel for window configuration events (resize, orientation)
configChan := make(chan ui.ConfigEvent)
// Initial screen size
screenWidth := unit.Dp(390)
screenHeight := unit.Dp(844)
// Compute initial elements
elems := ui.EditorLayout(screenWidth, screenHeight)
for {
switch e := w.Event().(type) {
case app.DestroyEvent:
return e.Err
case app.ConfigEvent:
// Send config event to logic goroutine (here, the main goroutine)
configChan <- ui.ConfigEvent{
Width: unit.Dp(e.Config.Size.X),
Height: unit.Dp(e.Config.Size.Y),
}
case app.FrameEvent:
// Process any pending config events
select {
case cfg := <-configChan:
// Update screen size and recompute layout
screenWidth = cfg.Width
screenHeight = cfg.Height
elems = ui.EditorLayout(screenWidth, screenHeight)
default:
// No config event, use existing elements
}
gtx := app.NewContext(&ops, e)
renderer.Draw(gtx, elems)
e.Frame(&ops)
}
}
}
// starterElements returns a hardcoded set of elements for initial testing.
func starterElements() []ui.Element {
return []ui.Element{
ui.NewLabel("Pad", 20, ui.Region{X: 0, Y: 0, W: 390, H: 48}),
ui.NewListView(ui.Region{X: 0, Y: 48, W: 390, H: 700},
[]ui.ListItem{
{Text: "notes.txt"},
{Text: "ideas.txt"},
{Text: "todo.txt"},
}),
}
}

View File

@ -23,22 +23,57 @@ const (
AlignEnd
)
// InputType specifies the type of user input event.
type InputType int
const (
Tap InputType = iota
DoubleTap
LongPress
Scroll
KeyDown
KeyUp
)
// InputEvent represents a user input event routed to an element.
type InputEvent struct {
ElementID string
Type InputType
Data any
}
// ConfigEvent represents a window configuration change (resize, orientation).
type ConfigEvent struct {
Width unit.Dp
Height unit.Dp
}
// Label displays static text.
type Label struct {
id string
region Region
visible bool
Text string
Align TextAlign
id string
region Region
visible bool
Text string
Align TextAlign
FontSize unit.Sp // 0 = theme default
Color Color // 0 = theme default
Bold bool
Color Color // 0 = theme default
Bold bool
}
func (l Label) Region() Region { return l.region }
func (l Label) Visible() bool { return l.visible }
func (l Label) ID() string { return l.id }
// NewLabel creates a visible Label element.
func NewLabel(text string, fontSize unit.Sp, region Region) Label {
return Label{
region: region,
visible: true,
Text: text,
FontSize: fontSize,
}
}
// TextField accepts text input or displays multiline text.
type TextField struct {
id string
@ -49,12 +84,21 @@ type TextField struct {
Focused bool
Multiline bool
ScrollOffset unit.Dp
VisibleLines []Line
WordWrap bool
WrapWidth unit.Dp
}
func (tf TextField) Region() Region { return tf.region }
func (tf TextField) Visible() bool { return tf.visible }
func (tf TextField) ID() string { return tf.id }
// Line represents a single line of text in a multiline TextField.
type Line struct {
Text string
LineNumber int // 1-indexed, for display
}
// ListView displays a scrollable list of items.
type ListView struct {
id string
@ -76,6 +120,229 @@ type ListItem struct {
Selected bool
}
// AlphaIndex displays an alphabetical index for quick navigation.
type AlphaIndex struct {
id string
region Region
visible bool
Letters []string
ActiveLetter string
}
func (ai AlphaIndex) Region() Region { return ai.region }
func (ai AlphaIndex) Visible() bool { return ai.visible }
func (ai AlphaIndex) ID() string { return ai.id }
// NewAlphaIndex creates a visible AlphaIndex element.
func NewAlphaIndex(region Region, letters []string) AlphaIndex {
return AlphaIndex{
region: region,
visible: true,
Letters: letters,
}
}
// Button is an interactive button element.
type Button struct {
id string
region Region
visible bool
Text string
Enabled bool
Primary bool
}
func (b Button) Region() Region { return b.region }
func (b Button) Visible() bool { return b.visible }
func (b Button) ID() string { return b.id }
// NewButton creates a visible Button element.
func NewButton(text string, enabled bool, primary bool, region Region) Button {
return Button{
region: region,
visible: true,
Text: text,
Enabled: enabled,
Primary: primary,
}
}
// SearchBar displays an in-editor search interface.
type SearchBar struct {
id string
region Region
visible bool
Query string
Match int
Total int
Forward bool
}
func (sb SearchBar) Region() Region { return sb.region }
func (sb SearchBar) Visible() bool { return sb.visible }
func (sb SearchBar) ID() string { return sb.id }
// NewSearchBar creates a visible SearchBar element.
func NewSearchBar(region Region, query string, match, total int, forward bool) SearchBar {
return SearchBar{
region: region,
visible: true,
Query: query,
Match: match,
Total: total,
Forward: forward,
}
}
// Cursor displays the text cursor and optional selection highlight.
type Cursor struct {
id string
region Region
visible bool
Line int
Column int
Blinking bool
Selection *Selection
}
func (c Cursor) Region() Region { return c.region }
func (c Cursor) Visible() bool { return c.visible }
func (c Cursor) ID() string { return c.id }
// Selection represents a text selection range.
type Selection struct {
StartLine, StartCol int
EndLine, EndCol int
}
// MergeHunk displays a conflict resolution hunk.
type MergeHunk struct {
id string
region Region
visible bool
HunkNumber int
TotalHunks int
LineRange string
ContextLines []string
OurLines []string
TheirLines []string
Resolution HunkResolution
}
func (mh MergeHunk) Region() Region { return mh.region }
func (mh MergeHunk) Visible() bool { return mh.visible }
func (mh MergeHunk) ID() string { return mh.id }
// HunkResolution represents the resolution state of a merge hunk.
type HunkResolution int
const (
Unresolved HunkResolution = iota
KeepOurs
KeepTheirs
MergeBoth
)
// StatusBar displays the top bar with filename, action icons, and conflict indicator.
type StatusBar struct {
id string
region Region
visible bool
Filename string
FilenameExp bool
CutCopy bool
Copy bool
Paste bool
ConflictIcon bool
Search bool
}
func (sb StatusBar) Region() Region { return sb.region }
func (sb StatusBar) Visible() bool { return sb.visible }
func (sb StatusBar) ID() string { return sb.id }
// NewStatusBar creates a visible StatusBar element.
func NewStatusBar(region Region, filename string, filenameExp bool, cutCopy, copy, paste, conflictIcon, search bool) StatusBar {
return StatusBar{
region: region,
visible: true,
Filename: filename,
FilenameExp: filenameExp,
CutCopy: cutCopy,
Copy: copy,
Paste: paste,
ConflictIcon: conflictIcon,
Search: search,
}
}
// BottomBar displays the bottom bar with cursor position, byte position, and word wrap toggle.
type BottomBar struct {
id string
region Region
visible bool
CursorPos string
BytePos string
WordWrap bool
}
func (bb BottomBar) Region() Region { return bb.region }
func (bb BottomBar) Visible() bool { return bb.visible }
func (bb BottomBar) ID() string { return bb.id }
// NewBottomBar creates a visible BottomBar element.
func NewBottomBar(region Region, cursorPos, bytePos string, wordWrap bool) BottomBar {
return BottomBar{
region: region,
visible: true,
CursorPos: cursorPos,
BytePos: bytePos,
WordWrap: wordWrap,
}
}
// Toast displays a temporary notification.
type Toast struct {
id string
region Region
visible bool
Text string
Timeout int // milliseconds
}
func (t Toast) Region() Region { return t.region }
func (t Toast) Visible() bool { return t.visible }
func (t Toast) ID() string { return t.id }
// NewToast creates a visible Toast element.
func NewToast(region Region, text string, timeout int) Toast {
return Toast{
region: region,
visible: true,
Text: text,
Timeout: timeout,
}
}
// Spacer adds vertical or horizontal space.
type Spacer struct {
id string
region Region
visible bool
}
func (s Spacer) Region() Region { return s.region }
func (s Spacer) Visible() bool { return s.visible }
func (s Spacer) ID() string { return s.id }
// NewSpacer creates a visible Spacer element.
func NewSpacer(height unit.Dp) Spacer {
return Spacer{
region: Region{H: height},
visible: true,
}
}
// Color is an RGBA color.
type Color struct {
R, G, B, A uint8
@ -85,22 +352,3 @@ type Color struct {
type Theme struct {
FontSize unit.Sp
}
// NewLabel creates a visible Label element.
func NewLabel(text string, fontSize unit.Sp, region Region) Label {
return Label{
region: region,
visible: true,
Text: text,
FontSize: fontSize,
}
}
// NewListView creates a visible ListView element.
func NewListView(region Region, items []ListItem) ListView {
return ListView{
region: region,
visible: true,
Items: items,
}
}

61
internal/ui/layout.go Normal file
View File

@ -0,0 +1,61 @@
package ui
import "gioui.org/unit"
const (
// Standard dimensions in Dp
statusBarLineHeight = unit.Dp(24)
statusBarFilenameLine = unit.Dp(24)
statusBarIconsLine = unit.Dp(24)
bottomBarHeight = unit.Dp(24)
iconSize = unit.Dp(24)
iconGap = unit.Dp(36)
padding = unit.Dp(8)
buttonPadding = unit.Dp(8)
)
// EditorLayout computes regions for the editor page.
// It takes screen dimensions and returns []Element.
func EditorLayout(screenWidth, screenHeight unit.Dp) []Element {
// Compute StatusBar region
// Line 1: filename (24 DP)
// Line 2: icons (24 DP)
statusBarHeight := statusBarFilenameLine + statusBarIconsLine
statusBarRegion := Region{
X: 0,
Y: 0,
W: screenWidth,
H: statusBarHeight,
}
// Compute BottomBar region (fixed height, at bottom of screen)
bottomBarY := screenHeight - bottomBarHeight
bottomBarRegion := Region{
X: 0,
Y: bottomBarY,
W: screenWidth,
H: bottomBarHeight,
}
// Create StatusBar with mocked filename
statusBar := NewStatusBar(
statusBarRegion,
"very_long_filename_that_does_not_fit_on_a_single_line.txt",
false, // FilenameExp
true, // CutCopy
false, // Copy
true, // Paste
false, // ConflictIcon
true, // Search
)
// Create BottomBar with mocked data
bottomBar := NewBottomBar(
bottomBarRegion,
"Ln 47, Col 12",
"1024 / 50000",
true, // WordWrap
)
return []Element{statusBar, bottomBar}
}

View File

@ -6,6 +6,7 @@ import (
"gioui.org/layout"
"gioui.org/op/clip"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget/material"
)
@ -31,6 +32,12 @@ func (r *Renderer) Draw(gtx layout.Context, elems []Element) {
r.drawLabel(gtx, v)
case ListView:
r.drawListView(gtx, v)
case StatusBar:
r.drawStatusBar(gtx, v)
case BottomBar:
r.drawBottomBar(gtx, v)
case Button:
r.drawButton(gtx, v)
default:
// unknown element type, skip
}
@ -68,3 +75,99 @@ func (r *Renderer) drawListView(gtx layout.Context, lv ListView) {
}
c.Pop()
}
func (r *Renderer) drawStatusBar(gtx layout.Context, sb StatusBar) {
reg := sb.Region()
th := material.NewTheme()
th.Shaper = r.shp
// Line 1: Filename (truncated with ellipsis if needed)
filenameLineY := reg.Y
filenameLineH := unit.Dp(24)
filenameW := reg.W - unit.Dp(16) // 8 DP padding on each side
// Truncate filename if needed
displayFilename := sb.Filename
// Simple truncation: if filename is too long, truncate it
if len(displayFilename) > 30 {
displayFilename = displayFilename[:27] + "..."
}
clipFilename := clip.Rect{
Min: image.Point{X: int(reg.X + unit.Dp(8)), Y: int(filenameLineY)},
Max: image.Point{X: int(reg.X + filenameW), Y: int(filenameLineY + filenameLineH)},
}.Push(gtx.Ops)
material.Label(th, r.theme.FontSize, displayFilename).Layout(gtx)
clipFilename.Pop()
// Line 2: Icons
iconsLineY := filenameLineY + filenameLineH
// Left side: Cut, Copy, Paste icons
leftX := reg.X + unit.Dp(8)
iconY := iconsLineY
if sb.CutCopy {
r.drawIconButton(gtx, th, "✂", leftX, iconY)
leftX += unit.Dp(36)
}
if sb.Copy {
r.drawIconButton(gtx, th, "⎘", leftX, iconY)
leftX += unit.Dp(36)
}
if sb.Paste {
r.drawIconButton(gtx, th, "⎘", leftX, iconY)
leftX += unit.Dp(36)
}
// Right side: Conflict, Search icons
rightX := reg.X + reg.W - unit.Dp(8)
if sb.Search {
rightX -= unit.Dp(36)
r.drawIconButton(gtx, th, "🔍", rightX, iconY)
}
if sb.ConflictIcon {
rightX -= unit.Dp(36)
r.drawIconButton(gtx, th, "⚠", rightX, iconY)
}
}
func (r *Renderer) drawBottomBar(gtx layout.Context, bb BottomBar) {
th := material.NewTheme()
th.Shaper = r.shp
// Left: Cursor position
material.Label(th, r.theme.FontSize, bb.CursorPos).Layout(gtx)
// Center: Byte position
material.Label(th, r.theme.FontSize, bb.BytePos).Layout(gtx)
// Right: Word wrap button
material.Label(th, r.theme.FontSize, "[Word Wrap: "+boolToString(bb.WordWrap)+"]").Layout(gtx)
}
func (r *Renderer) drawButton(gtx layout.Context, btn Button) {
th := material.NewTheme()
th.Shaper = r.shp
// Draw button text
material.Label(th, r.theme.FontSize, btn.Text).Layout(gtx)
}
func (r *Renderer) drawIconButton(gtx layout.Context, th *material.Theme, icon string, x, y unit.Dp) {
// Draw icon button
iconRegion := Region{X: x, Y: y, W: unit.Dp(24), H: unit.Dp(24)}
c := clip.Rect{
Min: image.Point{X: int(iconRegion.X), Y: int(iconRegion.Y)},
Max: image.Point{X: int(iconRegion.X + iconRegion.W), Y: int(iconRegion.Y + iconRegion.H)},
}.Push(gtx.Ops)
material.Label(th, r.theme.FontSize, icon).Layout(gtx)
c.Pop()
}
func boolToString(b bool) string {
if b {
return "On"
}
return "Off"
}

BIN
pad Executable file

Binary file not shown.