~pierrec/giox

c959a33e71c5d75ee41ba9fc4fa4fbc4c7e859bd — pierre 9 months ago d5ee559
materialx: added ModalAlertStyle

Signed-off-by: pierre <pierre.curto@gmail.com>
4 files changed, 242 insertions(+), 49 deletions(-)

M widgetx/file.go
A widgetx/materialx/modal.go
M widgetx/menu.go
A widgetx/modal.go
M widgetx/file.go => widgetx/file.go +2 -2
@@ 93,7 93,7 @@ type File struct {

	path   string
	data   fileData
	shadow Inactive
	shadow Modal
	errAt  time.Time
	err    error
	// Favorite directories.


@@ 978,7 978,7 @@ func (f *File) init() {
				},
			},
		}
		f.shadow = Inactive{
		f.shadow = Modal{
			Background: colorx.MulAlpha(f.Palettes.Area.Bg, 32),
		}


A widgetx/materialx/modal.go => widgetx/materialx/modal.go +177 -0
@@ 0,0 1,177 @@
package materialx

import (
	"image"
	"image/color"

	"gioui.org/f32"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"

	"git.sr.ht/~pierrec/giox/colorx"
	"git.sr.ht/~pierrec/giox/layoutx"
	"git.sr.ht/~pierrec/giox/widgetx"
)

type ModalAlertStyle struct {
	Active     bool
	Modal      widgetx.Modal
	Background color.NRGBA
	Border     widget.Border
	// Title is displayed at the top of the modal.
	Title material.LabelStyle
	// Message is displayed after the title.
	Message material.LabelStyle
	// Actions are' displayed along their axis at the bottom of the modal.
	ActionAxis   layout.Axis
	ActionStyles []material.ButtonStyle
	clicks       []widget.Clickable
	actions      layout.List
}

// ModalConfirm creates a two actions (typically OK/Cancel) alert modal.
func ModalConfirm(th *material.Theme) ModalAlertStyle {
	action := material.Button(th, nil, "")
	styles := []material.ButtonStyle{action, action}
	styles[0].Text = "OK"
	styles[1].Text = "Cancel"
	return ModalAlertStyle{
		Modal: widgetx.Modal{
			Background: colorx.MulAlpha(th.Palette.Bg, 32),
		},
		Background: th.Palette.Bg,
		Border: widget.Border{
			Color:        th.Palette.ContrastBg,
			CornerRadius: th.TextSize.Scale(0.5),
			Width:        th.TextSize.Scale(0.25),
		},
		Title:        material.H5(th, ""),
		Message:      material.Body1(th, ""),
		ActionAxis:   layout.Horizontal,
		ActionStyles: styles,
	}
}

func (m *ModalAlertStyle) init() {
	m.actions.Axis = m.ActionAxis
	switch cn, an := len(m.clicks), len(m.ActionStyles); {
	case cn < an:
		// Grow the clicks.
		m.clicks = append(m.clicks, make([]widget.Clickable, an-cn)...)
		for i := range m.clicks[cn:] {
			m.ActionStyles[cn+i].Button = &m.clicks[cn+i]
		}
	case cn > an:
		// Avoid leaks.
		for i := range m.clicks[an:] {
			m.clicks[an+i] = widget.Clickable{}
			m.ActionStyles[an+i].Button = nil
		}
		m.clicks = m.clicks[:an]
	}
}

func (m *ModalAlertStyle) Layout(gtx layout.Context, title, msg string) layout.Dimensions {
	if !m.Active {
		return layout.Dimensions{}
	}
	m.init()
	m.Modal.Layout(gtx)
	macro := op.Record(gtx.Ops)
	dims := layout.Stack{
		Alignment: layout.Center,
	}.Layout(gtx,
		layout.Expanded(func(gtx layout.Context) layout.Dimensions {
			size := gtx.Constraints.Min
			// Fill the dialog background.
			r := f32.Rectangle{Max: layout.FPt(size)}
			radius := gtx.Metric.Px(m.Border.CornerRadius)
			shape := clip.UniformRRect(r, float32(radius))
			paint.FillShape(gtx.Ops, m.Background, shape.Op(gtx.Ops))
			// Disable clicks in the dialog.
			pointer.Rect(image.Rectangle{Max: size}).Add(gtx.Ops)
			pointer.InputOp{
				Tag:   m,
				Types: pointer.Press | pointer.Release | pointer.Move,
			}.Add(gtx.Ops)

			return layout.Dimensions{Size: size}
		}),
		layout.Stacked(func(gtx layout.Context) layout.Dimensions {
			return m.Border.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
				flex := layoutx.Flex{
					Flex: layout.Flex{
						Axis:    layout.Vertical,
						Spacing: layout.SpaceSides,
					},
				}
				// Separator between the displayed items.
				sep := flex.RigidCross(func(gtx layout.Context) layout.Dimensions {
					col := colorx.MulAlpha(m.Background, 32)
					width := gtx.Metric.Px(m.Border.Width)
					shape := clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, width)}
					paint.FillShape(gtx.Ops, col, shape.Op())
					return layout.Dimensions{Size: shape.Max}
				})
				padding := layout.UniformInset(unit.Dp(4))
				return flex.Layout(gtx,
					flex.Rigid(func(gtx layout.Context) layout.Dimensions {
						return padding.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
							style := m.Title
							style.Text = title
							return style.Layout(gtx)
						})
					}),
					sep,
					flex.Rigid(func(gtx layout.Context) layout.Dimensions {
						return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
							style := m.Message
							style.Text = msg
							return style.Layout(gtx)
						})
					}),
					sep,
					flex.Rigid(func(gtx layout.Context) layout.Dimensions {
						return padding.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
							return m.actions.Layout(gtx, len(m.ActionStyles), func(gtx layout.Context, idx int) layout.Dimensions {
								return m.ActionStyles[idx].Layout(gtx)
							})
						})
					}),
				)
			})
		}),
	)
	op.Defer(gtx.Ops, macro.Stop())
	return dims
}

func (m *ModalAlertStyle) Clicked() (pos int) {
	m.init()
	if m.Modal.Changed() {
		m.Active = false
		return -1
	}
	for i := range m.clicks {
		if m.clicks[i].Clicked() {
			m.Active = false
			return i
		}
	}
	return -1
}

func max(a int, b ...int) int {
	for _, x := range b {
		if x > a {
			a = x
		}
	}
	return a
}

M widgetx/menu.go => widgetx/menu.go +0 -47
@@ 4,7 4,6 @@ import (
	"image"
	"image/color"

	"gioui.org/io/key"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"


@@ 37,52 36,6 @@ type MenuPopup struct {
	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}
}

// Changed returns whether or not the area was clicked or ESC was pressed.
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.

A widgetx/modal.go => widgetx/modal.go +63 -0
@@ 0,0 1,63 @@
package widgetx

import (
	"image"
	"image/color"

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

// Modal displays a widget and deactivates its background by grabbing
// keyboard and mouse events as specified by the Release fields.
type Modal struct {
	Background   color.NRGBA
	ReleaseKeys  bool
	ReleaseMouse bool
	click        widget.Clickable
	changed      bool
}

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

// Layout displays the modal as a defer operation.
func (m *Modal) Layout(gtx layout.Context) layout.Dimensions {
	m.update(gtx)
	macro := op.Record(gtx.Ops)
	size := gtx.Constraints.Min
	if !m.ReleaseKeys {
		key.InputOp{Tag: m}.Add(gtx.Ops)
		key.FocusOp{Tag: m}.Add(gtx.Ops)
	}
	if !m.ReleaseMouse {
		pointer.Rect(image.Rectangle{Max: size}).Add(gtx.Ops)
		m.click.Layout(gtx)
	}
	paint.FillShape(gtx.Ops, m.Background, clip.Rect{Max: size}.Op())
	op.Defer(gtx.Ops, macro.Stop())
	return layout.Dimensions{Size: size}
}

// Changed returns whether or not the area was clicked or ESC was pressed.
func (m *Modal) Changed() bool {
	changed := m.changed
	m.changed = false
	return changed
}