Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions native/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ island's edge and its hover fill spans the 8px edge to edge.

Rows in a list stand `Space(0.5)` to `Space(1.5)` apart; groups of rows `Space(2)`.

A scrolling list lays its bar over the rows (`material.Overlay`), never beside them: a bar that takes its
own width pushes the rows' right edge in, and their hover lands farther from the right edge than the left.

The geometry is shared; the look is not. Every screen lays out on the sidebar's edge, padding and hover
shape, and keeps what makes it itself: the Pulls list keeps its seams between rows, the Settings pane its
reading column. Matching a screen to the sidebar means its spacing, never its decoration.
Expand All @@ -155,6 +158,11 @@ edge: the Settings pane sits at `settingsPanePadX` / `settingsPanePadY` (40px /
the corner at `settingsCloseInset` (14px). Every other screen body follows the sidebar: its content at
18px from the island's edge, the 8px edge plus a row's 10px (`pullsPadX` in Pulls).

### Loading

A skeleton mirrors the screen it stands for: the same insets, line heights and columns, a bar where each
piece of text will land, so nothing moves when the content arrives. A generic block of bars is a bug.

### Boxes inside a screen

A thread, a pending comment or a composer is a box on `Sidebar` with `RadiusMD` and `Space(3)` padding. Its
Expand Down
6 changes: 5 additions & 1 deletion native/cmd/lich-native/pulls_panels.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"gioui.org/layout"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"

"github.com/lichdotdev/lich/native/backend"
"github.com/lichdotdev/lich/native/i18n"
Expand Down Expand Up @@ -248,8 +249,11 @@ func (v *pullView) panelLayout(gtx C, th *ui.Theme, p *pullsScreen, d backend.PR
}

func (v *pullView) panelList(gtx C, th *ui.Theme, rows []layout.Widget) D {
list := th.ScrollArea(&v.panel)
// The bar overlays the rows so their hover keeps the same edge on both sides.
list.AnchorStrategy = material.Overlay
return layout.Inset{Top: ui.Space(1), Bottom: ui.Space(1)}.Layout(gtx, func(gtx C) D {
return th.ScrollArea(&v.panel).Layout(gtx, len(rows), func(gtx C, i int) D { return rows[i](gtx) })
return list.Layout(gtx, len(rows), func(gtx C, i int) D { return rows[i](gtx) })
})
}

Expand Down
158 changes: 158 additions & 0 deletions native/cmd/lich-native/pulls_skeleton.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package main

import (
"gioui.org/layout"
"gioui.org/unit"

"github.com/lichdotdev/lich/native/ui"
)

// The line heights the skeleton's bars sit in, so each bar takes the room of
// the text it stands for: text-lg, text-sm and text-xs, and the line variant's
// tab list.
var (
skeletonTitleLine = ui.Space(7)
skeletonSMLine = ui.Space(5)
skeletonXSLine = ui.Space(4)
skeletonTabsLine = ui.Space(9)
)

// skeletonTabWidths are the tab strip's triggers, Overview to Files.
var skeletonTabWidths = []unit.Dp{ui.Space(18), ui.Space(24), ui.Space(18), ui.Space(14)}

// detailSkeleton mirrors body while the pull request loads, skeletonHeading
// standing in for its title row: the tab strip and the open tab's content,
// each bar where its text will land.
func (v *pullView) detailSkeleton(th *ui.Theme) layout.Widget {
return func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layout.Inset{Left: pullsToolbarPad, Right: pullsToolbarPad, Top: ui.Space(4)}.Layout(gtx, func(gtx C) D {
return skeletonTabs(gtx, th)
})
}),
layout.Rigid(th.Separator),
layout.Flexed(1, func(gtx C) D {
if v.tabs.Selected > 0 {
return skeletonRows(gtx, th)
}
return skeletonOverview(gtx, th)
}),
)
}
}

// skeletonBar is a bar of width by height centred in a line of height line.
func skeletonBar(th *ui.Theme, width, line, height unit.Dp) layout.Widget {
pad := (line - height) / 2
return func(gtx C) D {
return layout.Inset{Top: pad, Bottom: pad}.Layout(gtx, th.Skeleton(width, height).Layout)
}
}

// skeletonHeading is heading's title over the stats line: state, branches
// and author.
func skeletonHeading(gtx C, th *ui.Theme) D {
return layout.Inset{Top: pullsHeadingTop}.Layout(gtx, func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(skeletonBar(th, ui.Space(120), skeletonTitleLine, ui.Space(5))),
ui.VGap(3),
layout.Rigid(func(gtx C) D {
return layout.Flex{}.Layout(gtx,
layout.Rigid(skeletonBar(th, ui.Space(14), skeletonXSLine, ui.Space(3))),
ui.Gap(4),
layout.Rigid(skeletonBar(th, ui.Space(48), skeletonXSLine, ui.Space(3))),
ui.Gap(4),
layout.Rigid(skeletonBar(th, ui.Space(24), skeletonXSLine, ui.Space(3))),
)
}),
)
})
}

// skeletonTabs is the line tab strip, a bar where each trigger's label sits.
func skeletonTabs(gtx C, th *ui.Theme) D {
var children []layout.FlexChild
for _, w := range skeletonTabWidths {
children = append(children, layout.Rigid(func(gtx C) D {
return layout.Inset{Left: ui.Space(2), Right: ui.Space(3)}.Layout(gtx, skeletonBar(th, w, skeletonTabsLine, ui.Space(3.5)))
}))
}
return layout.Flex{}.Layout(gtx, children...)
}

// skeletonOverview is overview's description beside its facts, stacked the
// same way on an island too narrow for both.
func skeletonOverview(gtx C, th *ui.Theme) D {
prose := func(gtx C) D {
gtx.Constraints.Max.X = min(gtx.Constraints.Max.X, gtx.Dp(overviewProseWidth+2*pullsPadX))
return layout.Inset{Left: pullsPadX, Right: pullsPadX, Top: ui.Space(5)}.Layout(gtx, func(gtx C) D {
full := gtx.Metric.PxToDp(gtx.Constraints.Max.X)
children := []layout.FlexChild{layout.Rigid(skeletonBar(th, ui.Space(24), skeletonXSLine, ui.Space(3))), ui.VGap(2)}
for _, w := range []float32{1, 0.94, 0.97, 0.6} {
children = append(children, layout.Rigid(skeletonBar(th, full*unit.Dp(w), skeletonSMLine, ui.Space(3.5))))
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
})
}
if gtx.Constraints.Max.X < gtx.Dp(overviewSplitWidth) {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layout.Inset{Left: pullsPadX, Right: pullsPadX, Top: ui.Space(5)}.Layout(gtx, func(gtx C) D { return skeletonFacts(gtx, th) })
}),
layout.Rigid(prose),
)
}
return layout.Flex{}.Layout(gtx,
layout.Flexed(1, prose),
layout.Rigid(func(gtx C) D { return verticalRule(gtx, th) }),
layout.Rigid(func(gtx C) D {
w := gtx.Dp(overviewFactsWidth)
gtx.Constraints.Min.X, gtx.Constraints.Max.X = w, w
return layout.Inset{Left: ui.Space(5), Right: ui.Space(5), Top: ui.Space(5)}.Layout(gtx, func(gtx C) D { return skeletonFacts(gtx, th) })
}),
)
}

// skeletonFacts is facts' sections, a heading over its value each.
func skeletonFacts(gtx C, th *ui.Theme) D {
var children []layout.FlexChild
for i, w := range []unit.Dp{ui.Space(32), ui.Space(40), ui.Space(28)} {
if i > 0 {
children = append(children, ui.VGap(5))
}
children = append(children,
layout.Rigid(skeletonBar(th, ui.Space(18), skeletonXSLine, ui.Space(3))),
ui.VGap(2),
layout.Rigid(skeletonBar(th, w, skeletonSMLine, ui.Space(3.5))),
)
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
}

// skeletonRows is panelList's rows as Conversation, Commits and Checks draw
// them: a glyph beside a line over a dimmer one.
func skeletonRows(gtx C, th *ui.Theme) D {
widths := []float32{0.3, 0.42, 0.25, 0.36, 0.3}
return layout.Inset{Top: ui.Space(1), Bottom: ui.Space(1)}.Layout(gtx, func(gtx C) D {
var rows []layout.FlexChild
for _, w := range widths {
rows = append(rows, layout.Rigid(func(gtx C) D {
return layout.Inset{Left: pullsPadX, Right: pullsPadX, Top: ui.Space(2), Bottom: ui.Space(2)}.Layout(gtx, func(gtx C) D {
full := gtx.Metric.PxToDp(gtx.Constraints.Max.X)
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(th.Skeleton(ui.Space(4), ui.Space(4)).Layout),
ui.Gap(3),
layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(skeletonBar(th, full*unit.Dp(w), skeletonSMLine, ui.Space(3.5))),
layout.Rigid(skeletonBar(th, full*unit.Dp(w)*0.8, skeletonXSLine, ui.Space(3))),
)
}),
)
})
}))
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...)
})
}
33 changes: 10 additions & 23 deletions native/cmd/lich-native/pulls_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,11 @@ func (v *pullView) layout(gtx C, th *ui.Theme, p *pullsScreen, act *pullsAction)
read := p.store.detail(p.project.Path, p.selected)
switch {
case read.val == nil && read.err != "":
return v.withToolbar(gtx, th, p, nil, act, func(gtx C) D { return notice(gtx, th, read.err) })
return v.withToolbar(gtx, th, p, nil, func(gtx C) D { return notice(gtx, th, read.err) })
case read.val == nil && !read.loaded, read.val == nil && read.loading:
return v.withToolbar(gtx, th, p, nil, act, func(gtx C) D { return detailSkeleton(gtx, th) })
return v.withToolbar(gtx, th, p, func(gtx C) D { return skeletonHeading(gtx, th) }, v.detailSkeleton(th))
case read.val == nil:
return v.withToolbar(gtx, th, p, nil, act, func(gtx C) D { return notice(gtx, th, i18n.T("pulls.pullsEmptyState.none")) })
return v.withToolbar(gtx, th, p, nil, func(gtx C) D { return notice(gtx, th, i18n.T("pulls.pullsEmptyState.none")) })
}
d := v.edit.shown(*read.val, read, p.store.lastStale())
if stillSettling(d) && !read.loading {
Expand All @@ -179,25 +179,24 @@ func (v *pullView) layout(gtx C, th *ui.Theme, p *pullsScreen, act *pullsAction)
v.edit.openForShot(v.shotEdit, d)
v.shotEdit = ""
}
return v.withToolbar(gtx, th, p, &d, act, func(gtx C) D { return v.body(gtx, th, p, d, act) })
heading := func(gtx C) D { return v.titleRow(gtx, th, p, d, act) }
return v.withToolbar(gtx, th, p, heading, func(gtx C) D { return v.body(gtx, th, p, d, act) })
}

// withToolbar lays the strip over w: the crumb, then the pull request's title
// and stats with its actions at the right on the title's line. An island too
// narrow for both on one line puts the actions on their own line above the
// title.
func (v *pullView) withToolbar(gtx C, th *ui.Theme, p *pullsScreen, d *backend.PRDetail, act *pullsAction, w layout.Widget) D {
// withToolbar lays the strip over w: the crumb, then heading, the pull
// request's title row or its skeleton, when there is one.
func (v *pullView) withToolbar(gtx C, th *ui.Theme, p *pullsScreen, heading, w layout.Widget) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layout.UniformInset(pullsToolbarPad).Layout(gtx, func(gtx C) D {
gtx.Constraints.Min.X = gtx.Constraints.Max.X
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D { return v.crumb(gtx, th, p) }),
layout.Rigid(func(gtx C) D {
if d == nil {
if heading == nil {
return D{}
}
return v.titleRow(gtx, th, p, *d, act)
return heading(gtx)
}),
)
})
Expand Down Expand Up @@ -576,15 +575,3 @@ func (v *pullView) tabStrip(gtx C, th *ui.Theme, p *pullsScreen, d backend.PRDet
}),
)
}

func detailSkeleton(gtx C, th *ui.Theme) D {
return layout.Inset{Left: pullsPadX, Right: pullsPadX}.Layout(gtx, func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(th.Skeleton(ui.Space(96), ui.Space(5)).Layout),
ui.VGap(4),
layout.Rigid(th.Skeleton(ui.Space(72), ui.Space(3)).Layout),
ui.VGap(6),
layout.Rigid(th.Skeleton(0, ui.Space(24)).Layout),
)
})
}
Loading