~gioverse/chat

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
9 2

[PATCH chat v3 0/8] row layout

Details
Message ID
<20210729060833.1305-1-jackmordaunt.dev@gmail.com>
DKIM signature
pass
Download raw message
Improves on v2 by using a better strategy for minimizing heap allocations.
See the final commit for details.

Thank!

Jack Mordaunt (8):
  layout: capture row logic
  layout: introduce unified row
  widget/material: split RowStyle into file
  widget/material: migrate RowStyle to use Row layout
  widget/plato: migrate RowStyle to use Row layout
  layout,widget/{material,plato}: correctly name spacing as a margin
  layout: space entire row evenly
  layout: minimize heap allocation

 layout/row.go              |  98 ++++++++++++++++++++++++++
 widget/material/message.go | 134 ------------------------------------
 widget/material/row.go     | 136 +++++++++++++++++++++++++++++++++++++
 widget/plato/row.go        |  79 +++++++++------------
 4 files changed, 266 insertions(+), 181 deletions(-)
 create mode 100644 layout/row.go
 create mode 100644 widget/material/row.go

-- 
2.32.0.windows.1

[PATCH chat v3 1/8] layout: capture row logic

Details
Message ID
<20210729060833.1305-2-jackmordaunt.dev@gmail.com>
In-Reply-To
<20210729060833.1305-1-jackmordaunt.dev@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +83 -0
Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
---
 layout/row.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 83 insertions(+)
 create mode 100644 layout/row.go

diff --git a/layout/row.go b/layout/row.go
new file mode 100644
index 0000000..5d7c337
--- /dev/null
+++ b/layout/row.go
@@ -0,0 +1,83 @@
package layout

import (
	"gioui.org/layout"
)

type (
	C = layout.Context
	D = layout.Dimensions
)

// Row lays out a central widget with gutters either side.
// The central widget can be arbitrarily aligned and gutters can have
// supplimentary widgets stacked atop them.
type Row struct {
	// Margin between rows.
	Margin VerticalMarginStyle
	// Padding around the central widget.
	Padding VerticalMarginStyle
	// Gutter handles the left-right gutters of the row that provides padding and
	// can contain other widgets.
	Gutter GutterStyle
	// Direction of widgets within this row.
	// Typically, non-local widgets are aligned W, and local widgets aligned E.
	Direction layout.Direction
}

// RowChild specifies a content widget and two gutter widgets either side.
// RowChild is used to layout composite rows made up of any number of interal
// rows.
type RowChild struct {
	Left    layout.Widget
	Content layout.Widget
	Right   layout.Widget
}

// FullRow returns a RowChild that lays out content with optional gutter widgets
// either side.
func FullRow(l, w, r layout.Widget) RowChild {
	return RowChild{
		Left:    l,
		Content: w,
		Right:   r,
	}
}

// ContentRow returns a RowChild that lays out a content with no gutter widgets.
func ContentRow(w layout.Widget) RowChild {
	return RowChild{Content: w}
}

// Layout the Row with any number of internal rows.
func (r *Row) Layout(gtx C, w ...RowChild) D {
	if r.Margin == (VerticalMarginStyle{}) {
		r.Margin = VerticalMargin()
	}
	if r.Padding == (VerticalMarginStyle{}) {
		r.Padding = VerticalMargin()
	}
	var fl = make([]layout.FlexChild, len(w))
	for ii := range w {
		ii := ii
		fl[ii] = layout.Rigid(func(gtx C) D {
			return r.Gutter.Layout(gtx,
				w[ii].Left,
				func(gtx C) D {
					return r.Direction.Layout(gtx, func(gtx C) D {
						return r.Padding.Layout(gtx, func(gtx C) D {
							if w[ii].Content == nil {
								return D{}
							}
							return w[ii].Content(gtx)
						})
					})
				},
				w[ii].Right,
			)
		})
	}
	return r.Margin.Layout(gtx, func(gtx C) D {
		return layout.Flex{Axis: layout.Vertical}.Layout(gtx, fl...)
	})
}
-- 
2.32.0.windows.1

[PATCH chat v3 2/8] layout: introduce unified row

Details
Message ID
<20210729060833.1305-3-jackmordaunt.dev@gmail.com>
In-Reply-To
<20210729060833.1305-1-jackmordaunt.dev@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +20 -14
Unified rows don't have any gutters, letting a row child span the full width of the row.

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
---
 layout/row.go | 34 ++++++++++++++++++++--------------
 1 file changed, 20 insertions(+), 14 deletions(-)

diff --git a/layout/row.go b/layout/row.go
index 5d7c337..ee6cb55 100644
--- a/layout/row.go
+++ b/layout/row.go
@@ -32,6 +32,7 @@ type RowChild struct {
	Left    layout.Widget
	Content layout.Widget
	Right   layout.Widget
	Unified bool
}

// FullRow returns a RowChild that lays out content with optional gutter widgets
@@ -49,29 +50,34 @@ func ContentRow(w layout.Widget) RowChild {
	return RowChild{Content: w}
}

// UnifiedRow ignores gutters, taking up all available space.
func UnifiedRow(w layout.Widget) RowChild {
	return RowChild{Content: w, Unified: true}
}

// Layout the Row with any number of internal rows.
func (r *Row) Layout(gtx C, w ...RowChild) D {
	if r.Margin == (VerticalMarginStyle{}) {
		r.Margin = VerticalMargin()
	}
	if r.Padding == (VerticalMarginStyle{}) {
		r.Padding = VerticalMargin()
func (r Row) Layout(gtx C, w ...RowChild) D {
	content := func(ii int) layout.Widget {
		return func(gtx C) D {
			if w[ii].Content == nil {
				return D{}
			}
			return r.Padding.Layout(gtx, func(gtx C) D {
				return w[ii].Content(gtx)
			})
		}
	}
	var fl = make([]layout.FlexChild, len(w))
	for ii := range w {
		ii := ii
		fl[ii] = layout.Rigid(func(gtx C) D {
			if w[ii].Unified {
				return content(ii)(gtx)
			}
			return r.Gutter.Layout(gtx,
				w[ii].Left,
				func(gtx C) D {
					return r.Direction.Layout(gtx, func(gtx C) D {
						return r.Padding.Layout(gtx, func(gtx C) D {
							if w[ii].Content == nil {
								return D{}
							}
							return w[ii].Content(gtx)
						})
					})
					return r.Direction.Layout(gtx, content(ii))
				},
				w[ii].Right,
			)
-- 
2.32.0.windows.1

[PATCH chat v3 3/8] widget/material: split RowStyle into file

Details
Message ID
<20210729060833.1305-4-jackmordaunt.dev@gmail.com>
In-Reply-To
<20210729060833.1305-1-jackmordaunt.dev@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +152 -134
Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
---
 widget/material/message.go | 134 --------------------------------
 widget/material/row.go     | 152 +++++++++++++++++++++++++++++++++++++
 2 files changed, 152 insertions(+), 134 deletions(-)
 create mode 100644 widget/material/row.go

diff --git a/widget/material/message.go b/widget/material/message.go
index 401e9aa..f6fec65 100644
--- a/widget/material/message.go
+++ b/widget/material/message.go
@@ -3,7 +3,6 @@ package material
import (
	"image"
	"image/color"
	"time"

	"gioui.org/io/pointer"
	"gioui.org/layout"
@@ -11,7 +10,6 @@ import (
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
	"gioui.org/x/component"
	"gioui.org/x/richtext"
	chatlayout "git.sr.ht/~gioverse/chat/layout"
	"git.sr.ht/~gioverse/chat/ninepatch"
@@ -42,18 +40,6 @@ type (
	D = layout.Dimensions
)

// RowConfig describes the aspects of a chat message relevant for
// displaying it within a widget.
type RowConfig struct {
	Sender  string
	Avatar  image.Image
	Content string
	SentAt  time.Time
	Image   image.Image
	Local   bool
	Status  string
}

// UserInfoStyle defines the presentation of information about a user.
// It can present the user's name and avatar with a space between them.
type UserInfoStyle struct {
@@ -105,96 +91,6 @@ func (ui UserInfoStyle) Layout(gtx C) D {
	)
}

// RowStyle configures the presentation of a chat message within
// a vertical list of chat messages.
type RowStyle struct {
	OuterMargin chatlayout.VerticalMarginStyle
	chatlayout.GutterStyle
	// Local indicates that the message was sent by the local user,
	// and should be right-aligned.
	Local bool
	// Time is the timestamp associated with the message.
	Time material.LabelStyle
	// StatusIcon is an optional icon that will be displayed to the right of
	// the message instead of its timestamp.
	StatusIcon *widget.Icon
	// IconSize defines the size of the StatusIcon (if it is set).
	IconSize unit.Value
	// StatusMessage defines a warning message to be displayed beneath the
	// chat message.
	StatusMessage material.LabelStyle
	// ContentMargin configures space around the chat bubble.
	ContentMargin chatlayout.VerticalMarginStyle
	// UserInfoStyle configures how the sender's information is displayed.
	UserInfoStyle
	// MessageStyle configures how the text and its background are presented.
	MessageStyle
	// Interaction holds the interactive state of this message.
	Interaction *chatwidget.Row
	// Menu configures the right-click context menu for this message.
	Menu component.MenuStyle
}

// NewRow creates a style type that can lay out the data for a message.
func NewRow(th *material.Theme, interact *chatwidget.Row, menu *component.MenuState, msg RowConfig) RowStyle {
	ms := RowStyle{
		OuterMargin:   chatlayout.VerticalMargin(),
		GutterStyle:   chatlayout.Gutter(),
		Time:          material.Body2(th, msg.SentAt.Local().Format("15:04")),
		Local:         msg.Local,
		IconSize:      unit.Dp(32),
		ContentMargin: chatlayout.VerticalMargin(),
		UserInfoStyle: UserInfo(th, &interact.UserInfo, msg.Sender, msg.Avatar),
		Interaction:   interact,
		Menu:          component.Menu(th, menu),
		MessageStyle:  Message(th, &interact.Message, msg.Content, msg.Image),
	}
	ms.UserInfoStyle.Local = msg.Local
	if msg.Status != "" {
		ms.StatusMessage = material.Body2(th, msg.Status)
		ms.StatusMessage.Color = DefaultDangerColor
		ms.StatusIcon = ErrorIcon
		ms.StatusIcon.Color = DefaultDangerColor
	}
	return ms
}

// Layout the message.
func (c RowStyle) Layout(gtx C) D {
	return c.OuterMargin.Layout(gtx, func(gtx C) D {
		messageAlignment := layout.W
		if c.Local {
			messageAlignment = layout.E
		}
		return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
			layout.Rigid(func(gtx C) D {
				return c.GutterStyle.Layout(gtx,
					nil,
					func(gtx C) D {
						return messageAlignment.Layout(gtx, c.UserInfoStyle.Layout)
					},
					nil,
				)
			}),
			layout.Rigid(func(gtx C) D {
				return c.GutterStyle.Layout(gtx,
					nil,
					func(gtx C) D {
						return messageAlignment.Layout(gtx, c.layoutBubble)
					},
					c.layoutTimeOrIcon,
				)
			}),
			layout.Rigid(func(gtx C) D {
				if c.StatusMessage.Text == "" {
					return D{}
				}
				return layout.E.Layout(gtx, c.StatusMessage.Layout)
			}),
		)
	})
}

// MessageStyle configures the presentation of a chat message.
type MessageStyle struct {
	// Interaction holds the stateful parts of this message.
@@ -304,36 +200,6 @@ func (m MessageStyle) Layout(gtx C) D {
	})
}

// layoutBubble lays out the chat bubble.
func (c RowStyle) layoutBubble(gtx C) D {
	return layout.Stack{}.Layout(gtx,
		layout.Stacked(func(gtx C) D {
			return c.ContentMargin.Layout(gtx, c.MessageStyle.Layout)
		}),
		layout.Expanded(func(gtx C) D {
			return c.Interaction.ContextArea.Layout(gtx, func(gtx C) D {
				gtx.Constraints.Min = image.Point{}
				return c.Menu.Layout(gtx)
			})
		}),
	)
}

// layoutTimeOrIcon lays out a status icon if one is set, and
// otherwise lays out the time the messages was sent.
func (c RowStyle) layoutTimeOrIcon(gtx C) D {
	return layout.Center.Layout(gtx, func(gtx C) D {
		if c.StatusIcon == nil {
			return c.Time.Layout(gtx)
		}
		sideLength := gtx.Px(c.IconSize)
		gtx.Constraints.Max.X = sideLength
		gtx.Constraints.Max.Y = sideLength
		gtx.Constraints.Min = gtx.Constraints.Constrain(gtx.Constraints.Min)
		return c.StatusIcon.Layout(gtx)
	})
}

// Luminance computes the relative brightness of a color, normalized between
// [0,1]. Ignores alpha.
func Luminance(c color.NRGBA) float64 {
diff --git a/widget/material/row.go b/widget/material/row.go
new file mode 100644
index 0000000..09b13d4
--- /dev/null
+++ b/widget/material/row.go
@@ -0,0 +1,152 @@
package material

import (
	"image"
	"time"

	"gioui.org/layout"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
	"gioui.org/x/component"
	chatlayout "git.sr.ht/~gioverse/chat/layout"
	chatwidget "git.sr.ht/~gioverse/chat/widget"
)

// RowStyle configures the presentation of a chat message within
// a vertical list of chat messages.
type RowStyle struct {
	OuterMargin chatlayout.VerticalMarginStyle
	chatlayout.GutterStyle
	// Local indicates that the message was sent by the local user,
	// and should be right-aligned.
	Local bool
	// Time is the timestamp associated with the message.
	Time material.LabelStyle
	// StatusIcon is an optional icon that will be displayed to the right of
	// the message instead of its timestamp.
	StatusIcon *widget.Icon
	// IconSize defines the size of the StatusIcon (if it is set).
	IconSize unit.Value
	// StatusMessage defines a warning message to be displayed beneath the
	// chat message.
	StatusMessage material.LabelStyle
	// ContentMargin configures space around the chat bubble.
	ContentMargin chatlayout.VerticalMarginStyle
	// UserInfoStyle configures how the sender's information is displayed.
	UserInfoStyle
	// MessageStyle configures how the text and its background are presented.
	MessageStyle
	// Interaction holds the interactive state of this message.
	Interaction *chatwidget.Row
	// Menu configures the right-click context menu for this message.
	Menu component.MenuStyle
}

// RowConfig describes the aspects of a chat message relevant for
// displaying it within a widget.
type RowConfig struct {
	Sender  string
	Avatar  image.Image
	Content string
	SentAt  time.Time
	Image   image.Image
	Local   bool
	Status  string
}

// NewRow creates a style type that can lay out the data for a message.
func NewRow(th *material.Theme, interact *chatwidget.Row, menu *component.MenuState, msg RowConfig) RowStyle {
	if interact == nil {
		interact = &chatwidget.Row{}
	}
	if menu == nil {
		menu = &component.MenuState{}
	}
	ms := RowStyle{
		OuterMargin:   chatlayout.VerticalMargin(),
		GutterStyle:   chatlayout.Gutter(),
		Time:          material.Body2(th, msg.SentAt.Local().Format("15:04")),
		Local:         msg.Local,
		IconSize:      unit.Dp(32),
		ContentMargin: chatlayout.VerticalMargin(),
		UserInfoStyle: UserInfo(th, &interact.UserInfo, msg.Sender, msg.Avatar),
		Interaction:   interact,
		Menu:          component.Menu(th, menu),
		MessageStyle:  Message(th, &interact.Message, msg.Content, msg.Image),
	}
	ms.UserInfoStyle.Local = msg.Local
	if msg.Status != "" {
		ms.StatusMessage = material.Body2(th, msg.Status)
		ms.StatusMessage.Color = DefaultDangerColor
		ms.StatusIcon = ErrorIcon
		ms.StatusIcon.Color = DefaultDangerColor
	}
	return ms
}

// Layout the message.
func (c RowStyle) Layout(gtx C) D {
	return c.OuterMargin.Layout(gtx, func(gtx C) D {
		messageAlignment := layout.W
		if c.Local {
			messageAlignment = layout.E
		}
		return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
			layout.Rigid(func(gtx C) D {
				return c.GutterStyle.Layout(gtx,
					nil,
					func(gtx C) D {
						return messageAlignment.Layout(gtx, c.UserInfoStyle.Layout)
					},
					nil,
				)
			}),
			layout.Rigid(func(gtx C) D {
				return c.GutterStyle.Layout(gtx,
					nil,
					func(gtx C) D {
						return messageAlignment.Layout(gtx, c.layoutBubble)
					},
					c.layoutTimeOrIcon,
				)
			}),
			layout.Rigid(func(gtx C) D {
				if c.StatusMessage.Text == "" {
					return D{}
				}
				return layout.E.Layout(gtx, c.StatusMessage.Layout)
			}),
		)
	})
}

// layoutBubble lays out the chat bubble.
func (c RowStyle) layoutBubble(gtx C) D {
	return layout.Stack{}.Layout(gtx,
		layout.Stacked(func(gtx C) D {
			return c.ContentMargin.Layout(gtx, c.MessageStyle.Layout)
		}),
		layout.Expanded(func(gtx C) D {
			return c.Interaction.ContextArea.Layout(gtx, func(gtx C) D {
				gtx.Constraints.Min = image.Point{}
				return c.Menu.Layout(gtx)
			})
		}),
	)
}

// layoutTimeOrIcon lays out a status icon if one is set, and
// otherwise lays out the time the messages was sent.
func (c RowStyle) layoutTimeOrIcon(gtx C) D {
	return layout.Center.Layout(gtx, func(gtx C) D {
		if c.StatusIcon == nil {
			return c.Time.Layout(gtx)
		}
		sideLength := gtx.Px(c.IconSize)
		gtx.Constraints.Max.X = sideLength
		gtx.Constraints.Max.Y = sideLength
		gtx.Constraints.Min = gtx.Constraints.Constrain(gtx.Constraints.Min)
		return c.StatusIcon.Layout(gtx)
	})
}
-- 
2.32.0.windows.1

[PATCH chat v3 4/8] widget/material: migrate RowStyle to use Row layout

Details
Message ID
<20210729060833.1305-5-jackmordaunt.dev@gmail.com>
In-Reply-To
<20210729060833.1305-1-jackmordaunt.dev@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +24 -40
Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
---
 widget/material/row.go | 64 ++++++++++++++++--------------------------
 1 file changed, 24 insertions(+), 40 deletions(-)

diff --git a/widget/material/row.go b/widget/material/row.go
index 09b13d4..923b777 100644
--- a/widget/material/row.go
+++ b/widget/material/row.go
@@ -16,8 +16,7 @@ import (
// RowStyle configures the presentation of a chat message within
// a vertical list of chat messages.
type RowStyle struct {
	OuterMargin chatlayout.VerticalMarginStyle
	chatlayout.GutterStyle
	chatlayout.Row
	// Local indicates that the message was sent by the local user,
	// and should be right-aligned.
	Local bool
@@ -31,8 +30,6 @@ type RowStyle struct {
	// StatusMessage defines a warning message to be displayed beneath the
	// chat message.
	StatusMessage material.LabelStyle
	// ContentMargin configures space around the chat bubble.
	ContentMargin chatlayout.VerticalMarginStyle
	// UserInfoStyle configures how the sender's information is displayed.
	UserInfoStyle
	// MessageStyle configures how the text and its background are presented.
@@ -64,18 +61,24 @@ func NewRow(th *material.Theme, interact *chatwidget.Row, menu *component.MenuSt
		menu = &component.MenuState{}
	}
	ms := RowStyle{
		OuterMargin:   chatlayout.VerticalMargin(),
		GutterStyle:   chatlayout.Gutter(),
		Row: chatlayout.Row{
			Margin:    chatlayout.VerticalMargin(),
			Padding:   chatlayout.VerticalMargin(),
			Gutter:    chatlayout.Gutter(),
			Direction: layout.W,
		},
		Time:          material.Body2(th, msg.SentAt.Local().Format("15:04")),
		Local:         msg.Local,
		IconSize:      unit.Dp(32),
		ContentMargin: chatlayout.VerticalMargin(),
		UserInfoStyle: UserInfo(th, &interact.UserInfo, msg.Sender, msg.Avatar),
		Interaction:   interact,
		Menu:          component.Menu(th, menu),
		MessageStyle:  Message(th, &interact.Message, msg.Content, msg.Image),
	}
	ms.UserInfoStyle.Local = msg.Local
	if msg.Local {
		ms.Row.Direction = layout.E
	}
	if msg.Status != "" {
		ms.StatusMessage = material.Body2(th, msg.Status)
		ms.StatusMessage.Color = DefaultDangerColor
@@ -87,45 +90,18 @@ func NewRow(th *material.Theme, interact *chatwidget.Row, menu *component.MenuSt

// Layout the message.
func (c RowStyle) Layout(gtx C) D {
	return c.OuterMargin.Layout(gtx, func(gtx C) D {
		messageAlignment := layout.W
		if c.Local {
			messageAlignment = layout.E
		}
		return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
			layout.Rigid(func(gtx C) D {
				return c.GutterStyle.Layout(gtx,
					nil,
					func(gtx C) D {
						return messageAlignment.Layout(gtx, c.UserInfoStyle.Layout)
					},
					nil,
				)
			}),
			layout.Rigid(func(gtx C) D {
				return c.GutterStyle.Layout(gtx,
					nil,
					func(gtx C) D {
						return messageAlignment.Layout(gtx, c.layoutBubble)
					},
					c.layoutTimeOrIcon,
				)
			}),
			layout.Rigid(func(gtx C) D {
				if c.StatusMessage.Text == "" {
					return D{}
				}
				return layout.E.Layout(gtx, c.StatusMessage.Layout)
			}),
		)
	})
	return c.Row.Layout(gtx,
		chatlayout.ContentRow(c.UserInfoStyle.Layout),
		chatlayout.FullRow(nil, c.layoutBubble, c.layoutTimeOrIcon),
		chatlayout.UnifiedRow(c.layoutStatusMessage),
	)
}

// layoutBubble lays out the chat bubble.
func (c RowStyle) layoutBubble(gtx C) D {
	return layout.Stack{}.Layout(gtx,
		layout.Stacked(func(gtx C) D {
			return c.ContentMargin.Layout(gtx, c.MessageStyle.Layout)
			return c.MessageStyle.Layout(gtx)
		}),
		layout.Expanded(func(gtx C) D {
			return c.Interaction.ContextArea.Layout(gtx, func(gtx C) D {
@@ -150,3 +126,11 @@ func (c RowStyle) layoutTimeOrIcon(gtx C) D {
		return c.StatusIcon.Layout(gtx)
	})
}

// layoutStatusMessage lays out status message text, if any.
func (c RowStyle) layoutStatusMessage(gtx C) D {
	if c.StatusMessage.Text == "" {
		return D{}
	}
	return layout.E.Layout(gtx, c.StatusMessage.Layout)
}
-- 
2.32.0.windows.1

[PATCH chat v3 5/8] widget/plato: migrate RowStyle to use Row layout

Details
Message ID
<20210729060833.1305-6-jackmordaunt.dev@gmail.com>
In-Reply-To
<20210729060833.1305-1-jackmordaunt.dev@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +32 -47
Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
---
 widget/plato/row.go | 79 ++++++++++++++++++---------------------------
 1 file changed, 32 insertions(+), 47 deletions(-)

diff --git a/widget/plato/row.go b/widget/plato/row.go
index 0c40d45..ee9f22e 100644
--- a/widget/plato/row.go
+++ b/widget/plato/row.go
@@ -21,8 +21,7 @@ import (
// In particular, RowStyle is repsonsible for gutters and anchoring of
// messages.
type RowStyle struct {
	OuterMargin chatlayout.VerticalMarginStyle
	chatlayout.GutterStyle
	chatlayout.Row
	// Local indicates that the message was sent by the local user,
	// and should be right-aligned.
	Local bool
@@ -31,8 +30,6 @@ type RowStyle struct {
	// StatusMessage defines a warning message to be displayed beneath the
	// chat message.
	StatusMessage material.LabelStyle
	// ContentMargin configures space around the chat bubble.
	ContentMargin chatlayout.VerticalMarginStyle
	// UserInfoStyle configures how the sender's information is displayed.
	UserInfoStyle
	// Avatar image for the user.
@@ -65,15 +62,18 @@ func NewRow(
) RowStyle {
	interact.Avatar.Cache(msg.Avatar)
	ms := RowStyle{
		OuterMargin: chatlayout.VerticalMargin(),
		GutterStyle: chatlayout.GutterStyle{
			LeftWidth:  unit.Dp(unit.Dp(12).V + DefaultAvatarSize.V),
			RightWidth: unit.Dp(unit.Dp(12).V + DefaultAvatarSize.V),
			Alignment:  layout.Start,
		Row: chatlayout.Row{
			Margin:  chatlayout.VerticalMargin(),
			Padding: chatlayout.VerticalMargin(),
			Gutter: chatlayout.GutterStyle{
				LeftWidth:  unit.Dp(unit.Dp(12).V + DefaultAvatarSize.V),
				RightWidth: unit.Dp(unit.Dp(12).V + DefaultAvatarSize.V),
				Alignment:  layout.Start,
			},
			Direction: layout.W,
		},
		Time:          material.Body2(th, msg.SentAt.Local().Format("15:04")),
		Local:         msg.Local,
		ContentMargin: chatlayout.VerticalMargin(),
		UserInfoStyle: UserInfo(th, msg.Sender),
		Avatar: chatmaterial.Image{
			Image: widget.Image{
@@ -101,54 +101,39 @@ func NewRow(
		}),
	}
	ms.UserInfoStyle.Local = msg.Local
	if msg.Local {
		ms.Row.Direction = layout.E
	}
	return ms
}

// Layout the message.
func (c RowStyle) Layout(gtx C) D {
	return c.OuterMargin.Layout(gtx, func(gtx C) D {
		alignment := layout.W
		if c.Local {
			alignment = layout.E
		}
		return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
			layout.Rigid(func(gtx C) D {
				return c.GutterStyle.Layout(gtx,
					nil,
					func(gtx C) D {
						return alignment.Layout(gtx, c.UserInfoStyle.Layout)
					},
					nil,
				)
			}),
			layout.Rigid(func(gtx C) D {
				return c.GutterStyle.Layout(gtx,
					func(gtx C) D {
						if !c.Local {
							return c.layoutAvatar(gtx)
						}
						return D{}
					},
					func(gtx C) D {
						return alignment.Layout(gtx, c.layoutBubble)
					},
					func(gtx C) D {
						if c.Local {
							return c.layoutAvatar(gtx)
						}
						return D{}
					},
				)
			}),
		)
	})
	return c.Row.Layout(gtx,
		chatlayout.ContentRow(c.UserInfoStyle.Layout),
		chatlayout.FullRow(
			func(gtx C) D {
				if c.Local {
					return D{}
				}
				return c.layoutAvatar(gtx)
			},
			c.layoutBubble,
			func(gtx C) D {
				if !c.Local {
					return D{}
				}
				return c.layoutAvatar(gtx)
			},
		),
	)
}

// layoutBubble lays out the chat bubble.
func (c RowStyle) layoutBubble(gtx C) D {
	return layout.Stack{}.Layout(gtx,
		layout.Stacked(func(gtx C) D {
			return c.ContentMargin.Layout(gtx, c.MessageStyle.Layout)
			return c.MessageStyle.Layout(gtx)
		}),
		layout.Expanded(func(gtx C) D {
			return c.Interaction.ContextArea.Layout(gtx, func(gtx C) D {
-- 
2.32.0.windows.1

[PATCH chat v3 6/8] layout,widget/{material,plato}: correctly name spacing as a margin

Details
Message ID
<20210729060833.1305-7-jackmordaunt.dev@gmail.com>
In-Reply-To
<20210729060833.1305-1-jackmordaunt.dev@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +9 -8
This kind of spacing is not internal to the row, and therefor should be
called 'margin', not 'padding'.

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
---
 layout/row.go          | 5 +++--
 widget/material/row.go | 8 ++++----
 widget/plato/row.go    | 4 ++--
 3 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/layout/row.go b/layout/row.go
index ee6cb55..92ebe2f 100644
--- a/layout/row.go
+++ b/layout/row.go
@@ -15,8 +15,9 @@ type (
type Row struct {
	// Margin between rows.
	Margin VerticalMarginStyle
	// Padding around the central widget.
	Padding VerticalMarginStyle
	// InternalMargin between internal rows.
	// Leave unset if you want to control spacing between RowChild individually.
	InternalMargin VerticalMarginStyle
	// Gutter handles the left-right gutters of the row that provides padding and
	// can contain other widgets.
	Gutter GutterStyle
diff --git a/widget/material/row.go b/widget/material/row.go
index 923b777..39ac5a4 100644
--- a/widget/material/row.go
+++ b/widget/material/row.go
@@ -62,10 +62,10 @@ func NewRow(th *material.Theme, interact *chatwidget.Row, menu *component.MenuSt
	}
	ms := RowStyle{
		Row: chatlayout.Row{
			Margin:    chatlayout.VerticalMargin(),
			Padding:   chatlayout.VerticalMargin(),
			Gutter:    chatlayout.Gutter(),
			Direction: layout.W,
			Margin:         chatlayout.VerticalMargin(),
			InternalMargin: chatlayout.VerticalMargin(),
			Gutter:         chatlayout.Gutter(),
			Direction:      layout.W,
		},
		Time:          material.Body2(th, msg.SentAt.Local().Format("15:04")),
		Local:         msg.Local,
diff --git a/widget/plato/row.go b/widget/plato/row.go
index ee9f22e..c85bac1 100644
--- a/widget/plato/row.go
+++ b/widget/plato/row.go
@@ -63,8 +63,8 @@ func NewRow(
	interact.Avatar.Cache(msg.Avatar)
	ms := RowStyle{
		Row: chatlayout.Row{
			Margin:  chatlayout.VerticalMargin(),
			Padding: chatlayout.VerticalMargin(),
			Margin:         chatlayout.VerticalMargin(),
			InternalMargin: chatlayout.VerticalMargin(),
			Gutter: chatlayout.GutterStyle{
				LeftWidth:  unit.Dp(unit.Dp(12).V + DefaultAvatarSize.V),
				RightWidth: unit.Dp(unit.Dp(12).V + DefaultAvatarSize.V),
-- 
2.32.0.windows.1

[PATCH chat v3 7/8] layout: space entire row evenly

Details
Message ID
<20210729060833.1305-8-jackmordaunt.dev@gmail.com>
In-Reply-To
<20210729060833.1305-1-jackmordaunt.dev@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +13 -13
Correctly reflect that this margin spaces the internal rows from each
other, not just the content widget.

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
---
 layout/row.go | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/layout/row.go b/layout/row.go
index 92ebe2f..8e2b3cd 100644
--- a/layout/row.go
+++ b/layout/row.go
@@ -63,25 +63,25 @@ func (r Row) Layout(gtx C, w ...RowChild) D {
			if w[ii].Content == nil {
				return D{}
			}
			return r.Padding.Layout(gtx, func(gtx C) D {
				return w[ii].Content(gtx)
			})
			return w[ii].Content(gtx)
		}
	}
	var fl = make([]layout.FlexChild, len(w))
	for ii := range w {
		ii := ii
		fl[ii] = layout.Rigid(func(gtx C) D {
			if w[ii].Unified {
				return content(ii)(gtx)
			}
			return r.Gutter.Layout(gtx,
				w[ii].Left,
				func(gtx C) D {
					return r.Direction.Layout(gtx, content(ii))
				},
				w[ii].Right,
			)
			return r.InternalMargin.Layout(gtx, func(gtx C) D {
				if w[ii].Unified {
					return content(ii)(gtx)
				}
				return r.Gutter.Layout(gtx,
					w[ii].Left,
					func(gtx C) D {
						return r.Direction.Layout(gtx, content(ii))
					},
					w[ii].Right,
				)
			})
		})
	}
	return r.Margin.Layout(gtx, func(gtx C) D {
-- 
2.32.0.windows.1

[PATCH chat v3 8/8] layout: minimize heap allocation

Details
Message ID
<20210729060833.1305-9-jackmordaunt.dev@gmail.com>
In-Reply-To
<20210729060833.1305-1-jackmordaunt.dev@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +12 -4
When number of RowChild is small, use a stack allocated array.

The hope is to define a static array large-enough to avoid needing a
heap allocation for the majority of use-cases.

If however the number of RowChild is large, we fallback to relying on
slice append to allocate as needed.

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
---
 layout/row.go | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/layout/row.go b/layout/row.go
index 8e2b3cd..609b32f 100644
--- a/layout/row.go
+++ b/layout/row.go
@@ -58,6 +58,15 @@ func UnifiedRow(w layout.Widget) RowChild {

// Layout the Row with any number of internal rows.
func (r Row) Layout(gtx C, w ...RowChild) D {
	var (
		// array is a stack allocated array to avoid heap allocation if number
		// of RowChild is small.
		// Otherwise, slice append will allocate as needed.
		// 16 is a magic number subject to change upon further analysis.
		array [16]layout.FlexChild
		// slice based on the stack allocated array.
		slice = array[0:0]
	)
	content := func(ii int) layout.Widget {
		return func(gtx C) D {
			if w[ii].Content == nil {
@@ -66,10 +75,9 @@ func (r Row) Layout(gtx C, w ...RowChild) D {
			return w[ii].Content(gtx)
		}
	}
	var fl = make([]layout.FlexChild, len(w))
	for ii := range w {
		ii := ii
		fl[ii] = layout.Rigid(func(gtx C) D {
		slice = append(slice, layout.Rigid(func(gtx C) D {
			return r.InternalMargin.Layout(gtx, func(gtx C) D {
				if w[ii].Unified {
					return content(ii)(gtx)
@@ -82,9 +90,9 @@ func (r Row) Layout(gtx C, w ...RowChild) D {
					w[ii].Right,
				)
			})
		})
		}))
	}
	return r.Margin.Layout(gtx, func(gtx C) D {
		return layout.Flex{Axis: layout.Vertical}.Layout(gtx, fl...)
		return layout.Flex{Axis: layout.Vertical}.Layout(gtx, slice...)
	})
}
-- 
2.32.0.windows.1
Details
Message ID
<CD5SVV1PIQRF.33XWC8QHJSLW5@vendetta>
In-Reply-To
<20210729060833.1305-1-jackmordaunt.dev@gmail.com> (view parent)
DKIM signature
fail
Download raw message
DKIM signature: fail
On Thu Jul 29, 2021 at 2:08 AM EDT, Jack Mordaunt wrote:
> Improves on v2 by using a better strategy for minimizing heap
> allocations.
> See the final commit for details.
>
> Thank!

Nice work! I've merged it, with a follow-on commit that fixes image
messages. We accidentally dropped the image data during one of these
refactors.

I checked and was unable to get the escape analysis to say that our
array goes to the heap, so I *think* we achieved our goal there.

Cheers,
Chris

>
> Jack Mordaunt (8):
> layout: capture row logic
> layout: introduce unified row
> widget/material: split RowStyle into file
> widget/material: migrate RowStyle to use Row layout
> widget/plato: migrate RowStyle to use Row layout
> layout,widget/{material,plato}: correctly name spacing as a margin
> layout: space entire row evenly
> layout: minimize heap allocation
>
> layout/row.go | 98 ++++++++++++++++++++++++++
> widget/material/message.go | 134 ------------------------------------
> widget/material/row.go | 136 +++++++++++++++++++++++++++++++++++++
> widget/plato/row.go | 79 +++++++++------------
> 4 files changed, 266 insertions(+), 181 deletions(-)
> create mode 100644 layout/row.go
> create mode 100644 widget/material/row.go
>
> --
> 2.32.0.windows.1
Reply to thread Export thread (mbox)