~pierrec/giox

38151c325c83682dc260d6eca5c7e5b6a973bf7b — pierre 9 months ago 5b5215e
renamed PopupList to MenuPopup
widgetx: added OnClick to Icons.Layout, removed Icons.CLicked

Signed-off-by: pierre <pierre.curto@gmail.com>
M cmd/iconx/main.go => cmd/iconx/main.go +15 -19
@@ 174,24 174,19 @@ func (ui *uiMain) init() {
	}
}

func (ui *uiMain) update() {
	for idx := range ui.menu.Icons {
		if !ui.menu.Clicked(idx) {
			continue
func (ui *uiMain) onClick(idx int) {
	switch root := ui.tree.Root(); idx {
	case menuFoldLess:
		ui.menu.Hide(menuFoldLess)
		ui.menu.Show(menuFoldMore)
		for _, id := range root {
			ui.tree.Close(id)
		}
		switch root := ui.tree.Root(); idx {
		case menuFoldLess:
			ui.menu.Hide(menuFoldLess)
			ui.menu.Show(menuFoldMore)
			for _, id := range root {
				ui.tree.Close(id)
			}
		case menuFoldMore:
			ui.menu.Hide(menuFoldMore)
			ui.menu.Show(menuFoldLess)
			for _, id := range root {
				ui.tree.Open(id)
			}
	case menuFoldMore:
		ui.menu.Hide(menuFoldMore)
		ui.menu.Show(menuFoldLess)
		for _, id := range root {
			ui.tree.Open(id)
		}
	}
}


@@ 237,7 232,6 @@ func (ui *uiMain) layoutOverlay(gtx layout.Context) {
func (ui *uiMain) Layout(gtx layout.Context) layout.Dimensions {
	defer ui.layoutOverlay(gtx)
	ui.init()
	ui.update()
	padding := layout.Inset{Right: unit.Dp(4)}
	return layout.Flex{
		Axis: layout.Vertical,


@@ 248,7 242,9 @@ func (ui *uiMain) Layout(gtx layout.Context) layout.Dimensions {
			}
			return layout.Flex{}.Layout(gtx,
				layout.Rigid(func(gtx layout.Context) layout.Dimensions {
					return padding.Layout(gtx, ui.menu.Layout)
					return padding.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
						return ui.menu.Layout(gtx, ui.onClick)
					})
				}),
				layout.Flexed(0.05, func(gtx layout.Context) layout.Dimensions {
					return padding.Layout(gtx, materialx.InputLayout(ui.th, &ui.num, "Num/column").Layout)

M widgetx/click.go => widgetx/click.go +0 -79
@@ 4,8 4,6 @@ import (
	"image"
	"image/color"

	"gioui.org/f32"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/op"


@@ 65,83 63,6 @@ type ClickIcon struct {
	click *widget.Bool
}

// PopupList provides a popup list.
type PopupList struct {
	// Wrap wraps the whole list.
	Wrap       func(layout.Context, layout.Widget) layout.Dimensions
	List       ClickList
	Background color.NRGBA
	Shadow     color.NRGBA
	Width      unit.Value
	Height     unit.Value
	click      widget.Clickable
	closed     bool
}

func (pl *PopupList) update(gtx layout.Context) {
	if pl.click.Clicked() {
		pl.closed = true
		op.InvalidateOp{}.Add(gtx.Ops)
		return
	}
	for _, ev := range gtx.Events(pl) {
		if e, ok := ev.(key.Event); ok && e.Name == key.NameEscape {
			pl.closed = true
			return
		}
	}
}

func (pl *PopupList) Layout(gtx layout.Context, pos f32.Point, num int, el layout.ListElement) layout.Dimensions {
	pl.update(gtx)
	macro := op.Record(gtx.Ops)
	paint.FillShape(gtx.Ops, pl.Shadow, clip.Rect{Max: gtx.Constraints.Max}.Op())

	key.InputOp{Tag: pl}.Add(gtx.Ops)
	key.FocusOp{Tag: pl}.Add(gtx.Ops)
	cgtx := gtx
	cgtx.Constraints.Min = gtx.Constraints.Max
	pl.click.Layout(cgtx)

	op.Offset(pos).Add(gtx.Ops)
	gtx.Constraints.Min = image.Point{}
	dims := layout.Stack{}.Layout(gtx,
		layout.Stacked(func(gtx layout.Context) layout.Dimensions {
			size := image.Pt(
				gtx.Metric.Px(pl.Width),
				gtx.Metric.Px(pl.Height),
			)
			paint.FillShape(gtx.Ops, pl.Background, clip.Rect{Max: size}.Op())
			return layout.Dimensions{Size: size}
		}),
		layout.Expanded(func(gtx layout.Context) layout.Dimensions {
			gtx.Constraints.Max = gtx.Constraints.Min
			wrap := popupWrap
			if pl.Wrap != nil {
				wrap = pl.Wrap
			}
			return wrap(gtx, func(gtx layout.Context) layout.Dimensions {
				return pl.List.Layout(gtx, num, func(gtx layout.Context, idx int, click *widget.Clickable) layout.Dimensions {
					return el(gtx, idx)
				})
			})
		}),
	)
	op.Defer(gtx.Ops, macro.Stop())
	return dims
}

// Closed reports whether or not the popup was closed (single click outside of the popup or ESC key pressed).
func (pl *PopupList) Closed() bool {
	closed := pl.closed
	pl.closed = false
	return closed
}

func popupWrap(gtx layout.Context, w layout.Widget) layout.Dimensions {
	return w(gtx)
}

func (cl *ClickIcon) Bind(bar *Icons, idx int) {
	bar.init()
	cl.click = &bar.status[idx]

M widgetx/file.go => widgetx/file.go +70 -63
@@ 90,10 90,11 @@ type File struct {
	// Selection determines the number and kind of files or directories to be selected.
	Selection FileSelection

	path  string
	data  fileData
	errAt time.Time
	err   error
	path   string
	data   fileData
	shadow Inactive
	errAt  time.Time
	err    error
	// Favorite directories.
	favs   fileStringPanel
	favSep Resizable


@@ 390,38 391,36 @@ func (p *filePanel) refreshEntries() {
	p.Columns.Sort(p.entries)
}

func (p *filePanel) OnClick(idx int) {
	switch idx {
	case barCollapse:
		p.collapse()
	case barExpand:
		p.expand()
	case barSearch:
		p.Changed = true
		if !p.search.Active() {
			p.search.Clear()
		}
	case barList:
		p.Changed = true
		p.Columns.Active = !p.Columns.Active
		if p.Columns.Active {
			p.list.List.Axis = layout.Horizontal
		} else {
			p.list.List.Axis = layout.Vertical
		}
	case barHidden:
		p.Changed = true
	default:
		p.clicked = idx + 1
	}
}

func (p *filePanel) Update() {
	if p.search.Changed() {
		p.refreshEntries()
	}
	for idx := range p.Bar.Icons {
		if !p.Bar.Clicked(idx) {
			continue
		}
		switch idx {
		case barCollapse:
			p.collapse()
		case barExpand:
			p.expand()
		case barSearch:
			p.Changed = true
			if !p.search.Active() {
				p.search.Clear()
			}
		case barList:
			p.Changed = true
			p.Columns.Active = !p.Columns.Active
			if p.Columns.Active {
				p.list.List.Axis = layout.Horizontal
			} else {
				p.list.List.Axis = layout.Vertical
			}
		case barHidden:
			p.Changed = true
		default:
			p.clicked = idx + 1
		}
	}
	if p.Changed {
		p.Changed = false
		p.refreshEntries()


@@ 946,17 945,18 @@ func (f *File) init() {
	if f.path == "" {
		f.data = fileData{
			Border: f.Palettes.Main.ContrastBg,
			List: PopupList{
			List: MenuPopup{
				List: ClickList{
					Hover: f.Hover,
					List:  layout.List{Axis: layout.Vertical},
				},
				Background: f.Palettes.Main.Bg,
				Shadow:     colorx.MulAlpha(f.Palettes.Main.Bg, 32),
				Width:      unit.Px(300),
				Height:     unit.Px(300),
				Padding:    unit.Dp(4),
			},
		}
		f.shadow = Inactive{
			Background: colorx.MulAlpha(f.Palettes.Area.Bg, 32),
		}

		clickListWrap := ClickListWrap{
			Hover:     f.Hover,


@@ 1283,23 1283,12 @@ func (f *File) layoutFavRec(gtx layout.Context) layout.Dimensions {
}

func (f *File) layoutPaths(gtx layout.Context) layout.Dimensions {
	var menuDims layout.Dimensions
	defer func() {
		offset := image.Pt(
			barPartitions*menuDims.Size.X/len(f.pBar.Icons),
			menuDims.Size.Y,
		)
		f.data.Layout(gtx, offset, f.body1)
	}()
	return layout.Flex{
		Axis:      layout.Horizontal,
		Alignment: layout.Middle,
	}.Layout(gtx,
		layout.Rigid(func(gtx layout.Context) layout.Dimensions {
			for idx := range f.pBar.Icons {
				if !f.pBar.Clicked(idx) {
					continue
				}
			return f.pBar.Layout(gtx, func(idx int) {
				switch idx {
				case barFavRec:
					if f.pBar.Status(barFavRec) {


@@ 1316,10 1305,15 @@ func (f *File) layoutPaths(gtx layout.Context) layout.Dimensions {
					f.path = ""
				case barPartitions:
					f.data.Active = !f.data.Active
					op.InvalidateOp{}.Add(gtx.Ops)
				}
			}
			menuDims = f.pBar.Layout(gtx)
			return menuDims
			})
		}),
		layout.Rigid(func(gtx layout.Context) layout.Dimensions {
			// Show the popup next to the menu bar as
			// it is triggered by clicking on the last icon.
			f.data.Layout(gtx, f.body1)
			return layout.Dimensions{}
		}),
		layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
			return fileArea{


@@ 1349,23 1343,27 @@ func (f *File) layoutPaths(gtx layout.Context) layout.Dimensions {
}

func (f *File) layoutDirs(gtx layout.Context) layout.Dimensions {
	if f.dirs.Bar.Clicked(barNewDir) {
		ic := &f.dirs.Bar.Icons[barNewDir]
		if f.dirs.new.Active() {
			ic.Color.A = 128
		} else {
			ic.Color.A = 255
		}
	}
	f.dirs.Update()
	if f.dirs.Collapsed() {
		return f.dirs.Bar.Layout(gtx)
		return f.dirs.Bar.Layout(gtx, f.dirs.OnClick)
	}
	return layout.Flex{
		Axis: layout.Vertical,
	}.Layout(gtx,
		layout.Rigid(func(gtx layout.Context) layout.Dimensions {
			return f.dirs.Bar.Layout(gtx)
			return f.dirs.Bar.Layout(gtx, func(idx int) {
				switch idx {
				case barNewDir:
					ic := &f.dirs.Bar.Icons[barNewDir]
					if f.dirs.new.Active() {
						ic.Color.A = 128
					} else {
						ic.Color.A = 255
					}
				default:
					f.dirs.OnClick(idx)
				}
			})
		}),
		layout.Rigid(func(gtx layout.Context) layout.Dimensions {
			if f.dirs.new.input.Submitted() {


@@ 1413,13 1411,13 @@ func (f *File) layoutDirs(gtx layout.Context) layout.Dimensions {
func (f *File) layoutFiles(gtx layout.Context) layout.Dimensions {
	f.files.Update()
	if f.files.Collapsed() {
		return f.files.Bar.Layout(gtx)
		return f.files.Bar.Layout(gtx, f.files.OnClick)
	}
	return layout.Flex{
		Axis: layout.Vertical,
	}.Layout(gtx,
		layout.Rigid(func(gtx layout.Context) layout.Dimensions {
			return f.files.Bar.Layout(gtx)
			return f.files.Bar.Layout(gtx, f.files.OnClick)
		}),
		layout.Rigid(func(gtx layout.Context) layout.Dimensions {
			return f.files.search.Layout(gtx, f.Text.Editor)


@@ 1501,6 1499,15 @@ func (f *File) layoutSelected(gtx layout.Context) layout.Dimensions {
func (f *File) Layout(gtx layout.Context) layout.Dimensions {
	f.init()
	f.update()
	if f.shadow.Changed() {
		f.data.Active = false
	}
	if f.data.Active {
		// Deactivate the background for popups.
		gtx := gtx
		gtx.Constraints.Min = gtx.Constraints.Max
		f.shadow.Layout(gtx)
	}
	return colorx.Fill(gtx, f.Palettes.Area.Bg, func(gtx layout.Context) layout.Dimensions {
		return layout.Flex{
			Axis: layout.Vertical,

M widgetx/filedata.go => widgetx/filedata.go +26 -26
@@ 2,7 2,6 @@ package widgetx

import (
	"fmt"
	"image"
	"image/color"
	"os"
	"path/filepath"


@@ 10,8 9,7 @@ import (
	"strings"

	"gioui.org/layout"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/op"

	"github.com/shirou/gopsutil/disk"
)


@@ 23,8 21,14 @@ type fileData struct {
	Current    *disk.PartitionStat
	Dirs       []os.FileInfo
	Files      []os.FileInfo
	List       PopupList
	List       MenuPopup
	changed    bool
	cache      []cacheItem
}

type cacheItem struct {
	dims layout.Dimensions
	call op.CallOp
}

// Load loads data into fd.


@@ 35,6 39,7 @@ func (fd *fileData) Load(path string) (string, error) {
			return "", err
		}
		fd.Partitions = ps
		fd.cache = make([]cacheItem, len(ps))
	}

	path = filepath.ToSlash(filepath.Clean(path))


@@ 93,24 98,7 @@ func (fd *fileData) Load(path string) (string, error) {
	return path, nil
}

func (fd *fileData) init() {
	if fd.List.Wrap == nil {
		fd.List.Wrap = func(gtx layout.Context, w layout.Widget) layout.Dimensions {
			pad := unit.Dp(4)
			return widget.Border{
				Color: fd.Border,
				Width: pad.Scale(0.5),
			}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
				return layout.UniformInset(pad).Layout(gtx, w)
			})
		}
	}
}

func (fd *fileData) update(gtx layout.Context) {
	if fd.List.Closed() {
		fd.Active = false
	}
	// Item in the popup selected.
	if i, _ := fd.List.List.Clicked(); i >= 0 {
		fd.Active = false


@@ 119,15 107,27 @@ func (fd *fileData) update(gtx layout.Context) {
	}
}

func (fd *fileData) Layout(gtx layout.Context, offset image.Point, label func(layout.Context, string) layout.Dimensions) layout.Dimensions {
	fd.init()
func (fd *fileData) Layout(gtx layout.Context, label func(layout.Context, string) layout.Dimensions) layout.Dimensions {
	fd.update(gtx)
	if !fd.Active {
		return layout.Dimensions{}
	}
	return fd.List.Layout(gtx, layout.FPt(offset), len(fd.Partitions), func(gtx layout.Context, idx int) layout.Dimensions {
		gtx.Constraints.Min.X = gtx.Constraints.Max.X
		return label(gtx, fd.Partitions[idx].Mountpoint)
	var x int
	for i := range fd.cache {
		m := op.Record(gtx.Ops)
		dims := label(gtx, fd.Partitions[i].Mountpoint)
		fd.cache[i] = cacheItem{
			dims: dims,
			call: m.Stop(),
		}
		x = max(x, dims.Size.X)
	}
	gtx.Constraints.Min.X = x
	return fd.List.Layout(gtx, len(fd.Partitions), func(gtx layout.Context, idx int) layout.Dimensions {
		item := fd.cache[idx]
		item.call.Add(gtx.Ops)
		item.dims.Size.X = x
		return item.dims
	})
}


M widgetx/icons.go => widgetx/icons.go +14 -6
@@ 4,6 4,7 @@ import (
	"image/color"

	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/unit"
	"gioui.org/widget"
)


@@ 23,8 24,20 @@ func (s *Icons) init() {
	}
}

func (s *Icons) Layout(gtx layout.Context) layout.Dimensions {
func (s *Icons) Layout(gtx layout.Context, onClick func(idx int)) layout.Dimensions {
	s.init()
	if onClick != nil {
		var clicked bool
		for idx := range s.Icons {
			if s.status[idx].Changed() {
				clicked = true
				onClick(idx)
			}
		}
		if clicked {
			op.InvalidateOp{}.Add(gtx.Ops)
		}
	}
	return s.List.Layout(gtx, len(s.Icons), func(gtx layout.Context, idx int) layout.Dimensions {
		ic := &s.Icons[idx]
		if ic.Color.A == 0 {


@@ 47,11 60,6 @@ func (s *Icons) Click(idx int) {
	s.status[idx].Value = !s.status[idx].Value
}

func (s *Icons) Clicked(idx int) bool {
	s.init()
	return s.status[idx].Changed()
}

func (s *Icons) Hovered(idx int) bool {
	s.init()
	return s.status[idx].Hovered()

M widgetx/input.go => widgetx/input.go +1 -1
@@ 95,7 95,7 @@ func (in *Input) Text() string {
	return in.text
}

// Changed returns whether or not the file input has changed since last call.
// Changed returns whether or not the text input has changed since last call.
func (in *Input) Changed() bool {
	changed := in.changed
	in.changed = false

A widgetx/menu.go => widgetx/menu.go +118 -0
@@ 0,0 1,118 @@
package widgetx

import (
	"image"
	"image/color"

	"gioui.org/io/key"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/unit"
	"gioui.org/widget"
)

// listCache is used to record a widget and cache the recorded data.
type listCache struct {
	dims layout.Dimensions
	call op.CallOp
}

func listRecord(gtx layout.Context, w layout.Widget) listCache {
	m := op.Record(gtx.Ops)
	return listCache{
		dims: w(gtx),
		call: m.Stop(),
	}
}

// MenuPopup provides a popup menu.
type MenuPopup struct {
	List       ClickList
	Background color.NRGBA
	Padding    unit.Value
	click      widget.Clickable
	cache      []listCache
}

// Inactive deactivates an area by grabbing keyboard and mouse events
// as specified by the Release fields and changing the background color.
type Inactive struct {
	Background   color.NRGBA
	ReleaseKeys  bool
	ReleaseMouse bool
	click        widget.Clickable
	changed      bool
}

func (bk *Inactive) update(gtx layout.Context) {
	if bk.click.Clicked() {
		bk.changed = true
		op.InvalidateOp{}.Add(gtx.Ops)
		return
	}
	for _, ev := range gtx.Events(bk) {
		if e, ok := ev.(key.Event); ok && e.Name == key.NameEscape {
			bk.changed = true
			break
		}
	}
}

func (bk *Inactive) Layout(gtx layout.Context) layout.Dimensions {
	bk.update(gtx)
	macro := op.Record(gtx.Ops)
	if !bk.ReleaseKeys {
		key.InputOp{Tag: bk}.Add(gtx.Ops)
		key.FocusOp{Tag: bk}.Add(gtx.Ops)
	}
	if !bk.ReleaseMouse {
		bk.click.Layout(gtx)
	}
	paint.FillShape(gtx.Ops, bk.Background, clip.Rect{Max: gtx.Constraints.Min}.Op())
	op.Defer(gtx.Ops, macro.Stop())
	return layout.Dimensions{Size: gtx.Constraints.Min}
}

func (bk *Inactive) Changed() bool {
	changed := bk.changed
	bk.changed = false
	return changed
}

func (pl *MenuPopup) Layout(gtx layout.Context, num int, el layout.ListElement) layout.Dimensions {
	pl.List.init(num) // initialize the list as its elements are called now
	// Set the X axis min size to the largest item element.
	pl.cache = pl.cache[:0]
	var maxX int
	for i := 0; i < num; i++ {
		pl.cache = append(pl.cache, listRecord(gtx, func(gtx layout.Context) layout.Dimensions {
			return el(gtx, i)
		}))
		maxX = max(maxX, pl.cache[i].dims.Size.X)
	}
	for i := range pl.cache {
		pl.cache[i].dims.Size.X = maxX
	}

	macro := op.Record(gtx.Ops)
	gtx.Constraints.Min = image.Point{X: maxX}
	dims := layout.Stack{}.Layout(gtx,
		layout.Expanded(func(gtx layout.Context) layout.Dimensions {
			paint.FillShape(gtx.Ops, pl.Background, clip.Rect{Max: gtx.Constraints.Min}.Op())
			return layout.Dimensions{Size: gtx.Constraints.Min}
		}),
		layout.Stacked(func(gtx layout.Context) layout.Dimensions {
			return layout.UniformInset(pl.Padding).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
				return pl.List.Layout(gtx, num, func(gtx layout.Context, idx int, click *widget.Clickable) layout.Dimensions {
					item := pl.cache[idx]
					item.call.Add(gtx.Ops)
					return item.dims
				})
			})
		}),
	)
	op.Defer(gtx.Ops, macro.Stop())
	return dims
}