Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add slide action to slider and Update layout on overview page #256

Merged
merged 16 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 12 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
142 changes: 113 additions & 29 deletions ui/cryptomaterial/segmented_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import (
"github.com/crypto-power/cryptopower/ui/values"
)

type SegmentType int

const (
SegmentTypeGroup SegmentType = iota
SegmentTypeSplit
)

type SegmentedControl struct {
theme *Theme
list *ClickableList
Expand All @@ -21,22 +28,75 @@ type SegmentedControl struct {

changed bool
mu sync.Mutex

isSwipeActionEnabled bool
slideAction *SlideAction
slideActionTitle *SlideAction
segmentType SegmentType
}

func (t *Theme) SegmentedControl(segmentTitles []string) *SegmentedControl {
func (t *Theme) SegmentedControl(segmentTitles []string, segmentType SegmentType) *SegmentedControl {
list := t.NewClickableList(layout.Horizontal)
list.IsHoverable = false

return &SegmentedControl{
list: list,
theme: t,
segmentTitles: segmentTitles,
leftNavBtn: t.NewClickable(false),
rightNavBtn: t.NewClickable(false),
sc := &SegmentedControl{
list: list,
theme: t,
segmentTitles: segmentTitles,
leftNavBtn: t.NewClickable(false),
rightNavBtn: t.NewClickable(false),
isSwipeActionEnabled: true,
segmentType: segmentType,
slideAction: NewSlideAction(),
slideActionTitle: NewSlideAction(),
}

sc.slideAction.Draged(func(dragDirection SwipeDirection) {
isNext := dragDirection == SwipeLeft
sc.handleActionEvent(isNext)
})

sc.slideActionTitle.SetDragEffect(50)

sc.slideActionTitle.Draged(func(dragDirection SwipeDirection) {
isNext := dragDirection == SwipeLeft
sc.handleActionEvent(isNext)
})

return sc
}

func (sc *SegmentedControl) SetEnableSwipe(enable bool) {
sc.isSwipeActionEnabled = enable
}

func (sc *SegmentedControl) Layout(gtx C, body func(gtx C) D) D {
return UniformPadding(gtx, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
if sc.segmentType == SegmentTypeGroup {
return sc.GroupTileLayout(gtx)
}
return sc.splitTileLayout(gtx)
}),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: values.MarginPadding16}.Layout(gtx, func(gtx C) D {
if !sc.isSwipeActionEnabled {
return body(gtx)
}
return sc.slideAction.DragLayout(gtx, func(gtx C) D {
return sc.slideAction.TransformLayout(gtx, body)
})
})
}),
)
})
}

func (sc *SegmentedControl) Layout(gtx C) D {
func (sc *SegmentedControl) GroupTileLayout(gtx C) D {
sc.handleEvents()

return LinearLayout{
Expand All @@ -46,35 +106,37 @@ func (sc *SegmentedControl) Layout(gtx C) D {
Border: Border{Radius: Radius(8)},
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return sc.list.Layout(gtx, len(sc.segmentTitles), func(gtx C, i int) D {
isSelectedSegment := sc.SelectedIndex() == i
return layout.Center.Layout(gtx, func(gtx C) D {
bg := sc.theme.Color.SurfaceHighlight
txt := sc.theme.DecoratedText(values.TextSize16, sc.segmentTitles[i], sc.theme.Color.GrayText1, font.SemiBold)
border := Border{Radius: Radius(0)}
if isSelectedSegment {
bg = sc.theme.Color.Surface
txt.Color = sc.theme.Color.Text
border = Border{Radius: Radius(8)}
}
return LinearLayout{
Width: WrapContent,
Height: WrapContent,
Padding: layout.UniformInset(values.MarginPadding8),
Background: bg,
Margin: layout.UniformInset(values.MarginPadding5),
Border: border,
}.Layout2(gtx, txt.Layout)
return sc.slideActionTitle.DragLayout(gtx, func(gtx C) D {
return sc.list.Layout(gtx, len(sc.segmentTitles), func(gtx C, i int) D {
isSelectedSegment := sc.SelectedIndex() == i
return layout.Center.Layout(gtx, func(gtx C) D {
bg := sc.theme.Color.SurfaceHighlight
txt := sc.theme.DecoratedText(values.TextSize16, sc.segmentTitles[i], sc.theme.Color.GrayText1, font.SemiBold)
border := Border{Radius: Radius(0)}
if isSelectedSegment {
bg = sc.theme.Color.Surface
txt.Color = sc.theme.Color.Text
border = Border{Radius: Radius(8)}
}
return LinearLayout{
Width: WrapContent,
Height: WrapContent,
Padding: layout.UniformInset(values.MarginPadding8),
Background: bg,
Margin: layout.UniformInset(values.MarginPadding5),
Border: border,
}.Layout2(gtx, txt.Layout)
})
})
})
}),
)
}

func (sc *SegmentedControl) TransparentLayout(gtx C) D {
func (sc *SegmentedControl) splitTileLayout(gtx C) D {
sc.handleEvents()
return LinearLayout{
Width: gtx.Dp(values.MarginPadding600),
Width: gtx.Dp(values.MarginPadding700),
JustinBeBoy marked this conversation as resolved.
Show resolved Hide resolved
Height: WrapContent,
Orientation: layout.Horizontal,
Alignment: layout.Middle,
Expand Down Expand Up @@ -176,3 +238,25 @@ func (sc *SegmentedControl) SetSelectedSegment(segment string) {
}
}
}

func (sc *SegmentedControl) handleActionEvent(isNext bool) {
l := len(sc.segmentTitles) - 1 // index starts at 0
if isNext {
if sc.selectedIndex == l {
sc.selectedIndex = 0
} else {
sc.selectedIndex++
}
sc.slideAction.PushLeft()
sc.slideActionTitle.PushLeft()
} else {
if sc.selectedIndex == 0 {
sc.selectedIndex = l
} else {
sc.selectedIndex--
}
sc.slideAction.PushRight()
sc.slideActionTitle.PushRight()
}
sc.changed = true
}
214 changes: 214 additions & 0 deletions ui/cryptomaterial/slide_action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package cryptomaterial

import (
"image"
"time"

"gioui.org/f32"
"gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
)

const (
defaultDuration = 500 * time.Millisecond
defaultdragEffect = 100
)

type SwipeDirection int

const (
SwipeLeft SwipeDirection = iota
SwipeRight
)

type Dragged func(dragDirection SwipeDirection)

type SlideAction struct {
Duration time.Duration
IsReverse bool
push int
next *op.Ops
nextCall op.CallOp
lastCall op.CallOp

t0 time.Time
JustinBeBoy marked this conversation as resolved.
Show resolved Hide resolved
offset float32

// animation state
dragEffect int
dragStarted f32.Point
dragOffset int
drag gesture.Drag
draged Dragged
isPushing bool
}

func NewSlideAction() *SlideAction {
return &SlideAction{
Duration: defaultDuration,
dragEffect: defaultdragEffect,
}
}

// PushLeft pushes the existing widget to the left.
func (s *SlideAction) PushLeft() { s.push = 1 }

// PushRight pushes the existing widget to the right.
func (s *SlideAction) PushRight() { s.push = -1 }

func (s *SlideAction) SetDragEffect(offset int) { s.dragEffect = offset }

func (s *SlideAction) Draged(drag Dragged) {
s.draged = drag
}

func (s *SlideAction) DragLayout(gtx C, w layout.Widget) D {
if gtx.Queue != nil {
for _, event := range s.drag.Events(gtx.Metric, gtx.Queue, gesture.Horizontal) {
switch event.Type {
case pointer.Press:
s.dragStarted = event.Position
s.dragOffset = 0
case pointer.Drag:
newOffset := int(s.dragStarted.X - event.Position.X)
if newOffset > s.dragEffect {
if !s.isPushing && s.draged != nil {
s.isPushing = true
s.draged(SwipeLeft)
}
} else if newOffset < -s.dragEffect {
if !s.isPushing && s.draged != nil {
s.isPushing = true
s.draged(SwipeRight)
}
}
s.dragOffset = newOffset
case pointer.Release:
fallthrough
case pointer.Cancel:
s.isPushing = false
}
}
}
var dims layout.Dimensions
var call op.CallOp
{
m := op.Record(gtx.Ops)
dims = w(gtx)
call = m.Stop()
}

area := clip.Rect(image.Rect(0, 0, dims.Size.X, dims.Size.Y)).Push(gtx.Ops)
s.drag.Add(gtx.Ops)
defer area.Pop()

call.Add(gtx.Ops)
return dims
}

func (s *SlideAction) TransformLayout(gtx C, w layout.Widget) D {
if s.push != 0 {
s.next = nil
s.lastCall = s.nextCall
s.offset = float32(s.push)
s.t0 = gtx.Now
s.push = 0
}

var delta time.Duration
if !s.t0.IsZero() {
now := gtx.Now
delta = now.Sub(s.t0)
s.t0 = now
}

if s.offset != 0 {
duration := s.Duration
if duration == 0 {
duration = defaultDuration
}
movement := float32(delta.Seconds()) / float32(duration.Seconds())
if s.offset < 0 {
s.offset += movement
if s.offset >= 0 {
s.offset = 0
}
} else {
s.offset -= movement
if s.offset <= 0 {
s.offset = 0
}
}

op.InvalidateOp{}.Add(gtx.Ops)
}

var dims layout.Dimensions
{
if s.next == nil {
s.next = new(op.Ops)
}
gtx := gtx
gtx.Ops = s.next
gtx.Ops.Reset()
m := op.Record(gtx.Ops)
dims = w(gtx)
s.nextCall = m.Stop()
}

if s.offset == 0 {
s.nextCall.Add(gtx.Ops)
return dims
}

offset := smooth(s.offset)

reverse := 1
if s.IsReverse {
reverse = -1
}

if s.offset > 0 {
defer op.Offset(image.Point{
X: int(float32(dims.Size.X)*(offset-1)) * reverse,
}).Push(gtx.Ops).Pop()
s.lastCall.Add(gtx.Ops)

defer op.Offset(image.Point{
X: dims.Size.X * reverse,
}).Push(gtx.Ops).Pop()
s.nextCall.Add(gtx.Ops)
} else {
defer op.Offset(image.Point{
X: int(float32(dims.Size.X)*(offset+1)) * reverse,
}).Push(gtx.Ops).Pop()
s.lastCall.Add(gtx.Ops)

defer op.Offset(image.Point{
X: -dims.Size.X * reverse,
}).Push(gtx.Ops).Pop()
s.nextCall.Add(gtx.Ops)
}
return dims
}

// smooth handles -1 to 1 with ease-in-out cubic easing func.
func smooth(t float32) float32 {
if t < 0 {
return -easeInOutCubic(-t)
}
return easeInOutCubic(t)
}

// easeInOutCubic maps a linear value to a ease-in-out-cubic easing function.
// It is a mathematical function that describes how a value changes over time.
// It can be applied to adjusting the speed of animation
func easeInOutCubic(t float32) float32 {
if t < 0.5 {
return 4 * t * t * t
}
return (t-1)*(2*t-2)*(2*t-2) + 1
}
JustinBeBoy marked this conversation as resolved.
Show resolved Hide resolved
JustinBeBoy marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading