Added Popup show/hide animation

This commit is contained in:
Alexei Anoshenko 2024-11-25 22:13:29 +02:00
parent 5cc4475370
commit bc6e0c4db9
6 changed files with 273 additions and 102 deletions

View File

@ -79,7 +79,7 @@ ul:focus {
} }
.ruiPopupLayer { .ruiPopupLayer {
background-color: rgba(128,128,128,0.1); /*background-color: rgba(128,128,128,0.1);*/
position: absolute; position: absolute;
top: 0px; top: 0px;
bottom: 0px; bottom: 0px;

View File

@ -11,6 +11,14 @@ import (
// Color - represent color in argb format // Color - represent color in argb format
type Color uint32 type Color uint32
// ARGB creates Color using alpha, red, green and blue components
func ARGB[T int | uint | int8 | uint8](alpha, red, green, blue T) Color {
return ((Color(alpha) & 0xFF) << 24) +
((Color(red) & 0xFF) << 16) +
((Color(green) & 0xFF) << 8) +
(Color(blue) & 0xFF)
}
// ARGB - return alpha, red, green and blue components of the color // ARGB - return alpha, red, green and blue components of the color
func (color Color) ARGB() (uint8, uint8, uint8, uint8) { func (color Color) ARGB() (uint8, uint8, uint8, uint8) {
return uint8(color >> 24), return uint8(color >> 24),

286
popup.go
View File

@ -1,6 +1,7 @@
package rui package rui
import ( import (
"fmt"
"strings" "strings"
) )
@ -143,6 +144,48 @@ const (
// See `SizeUnit` description for more details. // See `SizeUnit` description for more details.
ArrowWidth PropertyName = "arrow-width" ArrowWidth PropertyName = "arrow-width"
// ShowTransform is the constant for "show-transform" property tag.
//
// Used by `Popup`.
// Specify start translation, scale and rotation over x, y and z axes as well as a distortion
// for an animated Popup showing/hidding.
//
// Supported types: `TransformProperty`, `string`.
//
// See `TransformProperty` description for more details.
//
// Conversion rules:
// `TransformProperty` - stored as is, no conversion performed.
// `string` - string representation of `Transform` interface. Example: "_{translate-x = 10px, scale-y = 1.1}".
ShowTransform = "show-transform"
// ShowDuration is the constant for "show-duration" property tag.
//
// Used by `Popup`.
// Sets the length of time in seconds that a Popup show/hide animation takes to complete.
//
// Supported types: `float`, `int`, `string`.
//
// Internal type is `float`, other types converted to it during assignment.
ShowDuration = "show-duration"
// ShowTiming is the constant for "show-timing" property tag.
//
// Used by `StackLayout`.
// Set how a Popup show/hide animation progresses through the duration of each cycle.
//
// Supported types: `string`.
//
// Values:
// "ease"(`EaseTiming`) - Speed increases towards the middle and slows down at the end.
// "ease-in"(`EaseInTiming`) - Speed is slow at first, but increases in the end.
// "ease-out"(`EaseOutTiming`) - Speed is fast at first, but decreases in the end.
// "ease-in-out"(`EaseInOutTiming`) - Speed is slow at first, but quickly increases and at the end it decreases again.
// "linear"(`LinearTiming`) - Constant speed.
ShowTiming = "show-timing"
ShowOpacity = "show-opacity"
// ArrowOffset is the constant for "arrow-offset" property tag. // ArrowOffset is the constant for "arrow-offset" property tag.
// //
// Used by `Popup`. // Used by `Popup`.
@ -219,14 +262,21 @@ type Popup interface {
html(buffer *strings.Builder) html(buffer *strings.Builder)
viewByHTMLID(id string) View viewByHTMLID(id string) View
keyEvent(event KeyEvent) bool keyEvent(event KeyEvent) bool
showAnimation()
dissmissAnimation(listener func(PropertyName)) bool
} }
type popupData struct { type popupData struct {
layerView View layerView GridLayout
view View popupView GridLayout
contentView View
buttons []PopupButton buttons []PopupButton
cancelable bool cancelable bool
dismissListener []func(Popup) dismissListener []func(Popup)
showTransform TransformProperty
showOpacity float64
showDuration float64
showTiming string
} }
type popupManager struct { type popupManager struct {
@ -397,39 +447,15 @@ func (arrow *popupArrow) createView(popupView View) View {
return NewGridLayout(session, params) return NewGridLayout(session, params)
} }
func (popup *popupData) init(view View, popupParams Params) { func (popup *popupData) layerCellWidth(arrowLocation int, popupParams Params, session Session) []SizeUnit {
popup.view = view
popup.cancelable = false
session := view.Session()
columnCount := 3 var columnCount int
rowCount := 3 switch arrowLocation {
popupRow := 1 case LeftArrow, RightArrow:
popupColumn := 1
arrow := popupArrow{
row: 1,
column: 1,
align: CenterAlign,
}
switch arrow.location, _ = enumProperty(popupParams, Arrow, session, NoneArrow); arrow.location {
case TopArrow:
rowCount = 4
popupRow = 2
case BottomArrow:
rowCount = 4
arrow.row = 2
case LeftArrow:
columnCount = 4 columnCount = 4
popupColumn = 2
case RightArrow:
columnCount = 4
arrow.column = 2
default: default:
columnCount = 3
} }
cellWidth := make([]SizeUnit, columnCount) cellWidth := make([]SizeUnit, columnCount)
@ -444,6 +470,19 @@ func (popup *popupData) init(view View, popupParams Params) {
cellWidth[0] = Fr(1) cellWidth[0] = Fr(1)
cellWidth[columnCount-1] = Fr(1) cellWidth[columnCount-1] = Fr(1)
} }
return cellWidth
}
func (popup *popupData) layerCellHeight(arrowLocation int, popupParams Params, session Session) []SizeUnit {
var rowCount int
switch arrowLocation {
case TopArrow, BottomArrow:
rowCount = 4
default:
rowCount = 3
}
cellHeight := make([]SizeUnit, rowCount) cellHeight := make([]SizeUnit, rowCount)
switch vAlign, _ := enumProperty(popupParams, VerticalAlign, session, CenterAlign); vAlign { switch vAlign, _ := enumProperty(popupParams, VerticalAlign, session, CenterAlign); vAlign {
@ -458,16 +497,47 @@ func (popup *popupData) init(view View, popupParams Params) {
cellHeight[rowCount-1] = Fr(1) cellHeight[rowCount-1] = Fr(1)
} }
return cellHeight
}
func (popup *popupData) init(view View, popupParams Params) {
popup.contentView = view
popup.cancelable = false
session := view.Session()
popupRow := 1
popupColumn := 1
arrow := popupArrow{
row: 1,
column: 1,
align: CenterAlign,
}
switch arrow.location, _ = enumProperty(popupParams, Arrow, session, NoneArrow); arrow.location {
case TopArrow:
popupRow = 2
case BottomArrow:
arrow.row = 2
case LeftArrow:
popupColumn = 2
case RightArrow:
arrow.column = 2
}
layerParams := Params{ layerParams := Params{
Style: "ruiPopupLayer", Style: "ruiPopupLayer",
MaxWidth: Percent(100), MaxWidth: Percent(100),
MaxHeight: Percent(100), MaxHeight: Percent(100),
CellWidth: cellWidth, CellWidth: popup.layerCellWidth(arrow.location, popupParams, session),
CellHeight: cellHeight, CellHeight: popup.layerCellHeight(arrow.location, popupParams, session),
} }
params := Params{ params := Params{
Style: "ruiPopup", Style: "ruiPopup",
ID: "ruiPopup",
Row: popupRow, Row: popupRow,
Column: popupColumn, Column: popupColumn,
MaxWidth: Percent(100), MaxWidth: Percent(100),
@ -483,10 +553,14 @@ func (popup *popupData) init(view View, popupParams Params) {
} }
var closeButton View = nil var closeButton View = nil
outsideClose := false
buttons := []PopupButton{}
titleStyle := "ruiPopupTitle"
var title View = nil var title View = nil
outsideClose := false
popup.buttons = []PopupButton{}
titleStyle := "ruiPopupTitle"
popup.showOpacity = 1.0
popup.showDuration = 1.0
popup.showTiming = "easy"
for tag, value := range popupParams { for tag, value := range popupParams {
if value != nil { if value != nil {
@ -532,10 +606,10 @@ func (popup *popupData) init(view View, popupParams Params) {
case Buttons: case Buttons:
switch value := value.(type) { switch value := value.(type) {
case PopupButton: case PopupButton:
buttons = []PopupButton{value} popup.buttons = []PopupButton{value}
case []PopupButton: case []PopupButton:
buttons = value popup.buttons = value
} }
case Title: case Title:
@ -587,13 +661,36 @@ func (popup *popupData) init(view View, popupParams Params) {
case ArrowOffset: case ArrowOffset:
arrow.off, _ = sizeProperty(popupParams, ArrowOffset, session) arrow.off, _ = sizeProperty(popupParams, ArrowOffset, session)
case ShowOpacity:
if opacity, _ := floatProperty(popupParams, ShowOpacity, session, 1); opacity >= 0 && opacity < 1 {
popup.showOpacity = opacity
}
case ShowTransform:
if transform := valueToTransformProperty(value); transform != nil && !transform.empty() {
popup.showTransform = transform
}
case ShowDuration:
if duration, _ := floatProperty(popupParams, ShowDuration, session, 1); duration > 0 {
popup.showDuration = duration
}
case ShowTiming:
if text, ok := value.(string); ok {
text, _ = session.resolveConstants(text)
if isTimingFunctionValid(text) {
popup.showTiming = text
}
}
default: default:
params[tag] = value params[tag] = value
} }
} }
} }
popupView := NewGridLayout(view.Session(), params) popup.popupView = NewGridLayout(view.Session(), params)
var popupCellHeight []SizeUnit var popupCellHeight []SizeUnit
viewRow := 0 viewRow := 0
@ -605,7 +702,7 @@ func (popup *popupData) init(view View, popupParams Params) {
if closeButton != nil { if closeButton != nil {
titleContent = append(titleContent, closeButton) titleContent = append(titleContent, closeButton)
} }
popupView.Append(NewGridLayout(session, Params{ popup.popupView.Append(NewGridLayout(session, Params{
Row: 0, Row: 0,
Style: titleStyle, Style: titleStyle,
CellWidth: []any{Fr(1), AutoSize()}, CellWidth: []any{Fr(1), AutoSize()},
@ -621,10 +718,9 @@ func (popup *popupData) init(view View, popupParams Params) {
} }
view.Set(Row, viewRow) view.Set(Row, viewRow)
popupView.Append(view) popup.popupView.Append(view)
popup.buttons = buttons if buttonCount := len(popup.buttons); buttonCount > 0 {
if buttonCount := len(buttons); buttonCount > 0 {
buttonsAlign, _ := enumProperty(params, ButtonsAlign, session, RightAlign) buttonsAlign, _ := enumProperty(params, ButtonsAlign, session, RightAlign)
popupCellHeight = append(popupCellHeight, AutoSize()) popupCellHeight = append(popupCellHeight, AutoSize())
gap, _ := sizeConstant(session, "ruiPopupButtonGap") gap, _ := sizeConstant(session, "ruiPopupButtonGap")
@ -641,7 +737,7 @@ func (popup *popupData) init(view View, popupParams Params) {
buttonsPanel.Set(Margin, gap) buttonsPanel.Set(Margin, gap)
} }
for i, button := range buttons { for i, button := range popup.buttons {
title := button.Title title := button.Title
if title == "" && button.Type == CancelButton { if title == "" && button.Type == CancelButton {
title = "Cancel" title = "Cancel"
@ -668,33 +764,82 @@ func (popup *popupData) init(view View, popupParams Params) {
buttonsPanel.Append(buttonView) buttonsPanel.Append(buttonView)
} }
popupView.Append(NewGridLayout(session, Params{ popup.popupView.Append(NewGridLayout(session, Params{
Row: viewRow + 1, Row: viewRow + 1,
CellHorizontalAlign: buttonsAlign, CellHorizontalAlign: buttonsAlign,
Content: buttonsPanel, Content: buttonsPanel,
})) }))
} }
popupView.Set(CellHeight, popupCellHeight) popup.popupView.Set(CellHeight, popupCellHeight)
if arrow.location != NoneArrow { if arrow.location != NoneArrow {
layerParams[Content] = []View{popupView, arrow.createView(popupView)} layerParams[Content] = []View{popup.popupView, arrow.createView(popup.popupView)}
} else { } else {
layerParams[Content] = []View{popupView} layerParams[Content] = []View{popup.popupView}
} }
popup.layerView = NewGridLayout(session, layerParams) popup.layerView = NewGridLayout(session, layerParams)
if popup.showOpacity != 1 || popup.showTransform != nil {
animation := NewAnimation(Params{
Duration: popup.showDuration,
TimingFunction: popup.showTiming,
})
if popup.showOpacity != 1 {
popup.popupView.Set(Opacity, popup.showOpacity)
popup.popupView.SetTransition(Opacity, animation)
}
if popup.showTransform != nil {
popup.popupView.Set(Transform, popup.showTransform)
popup.popupView.SetTransition(Transform, animation)
}
} else {
session.updateCSSProperty("ruiPopupLayer", "transition", "")
}
if outsideClose { if outsideClose {
popup.layerView.Set(ClickEvent, popup.cancel) popup.layerView.Set(ClickEvent, popup.cancel)
} }
} }
func (popup popupData) View() View { func (popup *popupData) showAnimation() {
return popup.view if popup.showOpacity != 1 || popup.showTransform != nil {
htmlID := popup.popupView.htmlID()
session := popup.Session()
if popup.showOpacity != 1 {
session.updateCSSProperty(htmlID, string(Opacity), "1")
}
if popup.showTransform != nil {
session.updateCSSProperty(htmlID, string(Transform), "")
}
}
}
func (popup *popupData) dissmissAnimation(listener func(PropertyName)) bool {
if popup.showOpacity != 1 || popup.showTransform != nil {
session := popup.Session()
popup.popupView.Set(TransitionEndEvent, listener)
popup.popupView.Set(TransitionCancelEvent, listener)
htmlID := popup.popupView.htmlID()
if popup.showOpacity != 1 {
session.updateCSSProperty(htmlID, string(Opacity), fmt.Sprintf("%.2f", popup.showOpacity))
}
if popup.showTransform != nil {
session.updateCSSProperty(htmlID, string(Transform), popup.showTransform.transformCSS(session))
}
return true
}
return false
}
func (popup *popupData) View() View {
return popup.contentView
} }
func (popup *popupData) Session() Session { func (popup *popupData) Session() Session {
return popup.view.Session() return popup.contentView.Session()
} }
func (popup *popupData) cancel() { func (popup *popupData) cancel() {
@ -709,9 +854,6 @@ func (popup *popupData) cancel() {
func (popup *popupData) Dismiss() { func (popup *popupData) Dismiss() {
popup.Session().popupManager().dismissPopup(popup) popup.Session().popupManager().dismissPopup(popup)
for _, listener := range popup.dismissListener {
listener(popup)
}
} }
func (popup *popupData) Show() { func (popup *popupData) Show() {
@ -719,7 +861,6 @@ func (popup *popupData) Show() {
} }
func (popup *popupData) html(buffer *strings.Builder) { func (popup *popupData) html(buffer *strings.Builder) {
viewHTML(popup.layerView, buffer, "") viewHTML(popup.layerView, buffer, "")
} }
@ -728,6 +869,8 @@ func (popup *popupData) viewByHTMLID(id string) View {
} }
func (popup *popupData) onDismiss() { func (popup *popupData) onDismiss() {
popup.Session().callFunc("removeView", popup.layerView.htmlID())
for _, listener := range popup.dismissListener { for _, listener := range popup.dismissListener {
listener(popup) listener(popup)
} }
@ -808,6 +951,7 @@ func (manager *popupManager) showPopup(popup Popup) {
session.updateCSSProperty("ruiTooltipLayer", "opacity", "0") session.updateCSSProperty("ruiTooltipLayer", "opacity", "0")
session.updateCSSProperty("ruiPopupLayer", "visibility", "visible") session.updateCSSProperty("ruiPopupLayer", "visibility", "visible")
session.updateCSSProperty("ruiRoot", "pointer-events", "none") session.updateCSSProperty("ruiRoot", "pointer-events", "none")
popup.showAnimation()
} }
func (manager *popupManager) dismissPopup(popup Popup) { func (manager *popupManager) dismissPopup(popup Popup) {
@ -821,31 +965,37 @@ func (manager *popupManager) dismissPopup(popup Popup) {
return return
} }
index := -1
for n, p := range manager.popups {
if p == popup {
index = n
break
}
}
if index < 0 {
return
}
session := popup.Session() session := popup.Session()
if manager.popups[count-1] == popup { listener := func(PropertyName) {
if index == count-1 {
if count == 1 { if count == 1 {
manager.popups = []Popup{} manager.popups = []Popup{}
session.updateCSSProperty("ruiRoot", "pointer-events", "auto") session.updateCSSProperty("ruiRoot", "pointer-events", "auto")
session.updateCSSProperty("ruiPopupLayer", "visibility", "hidden") session.updateCSSProperty("ruiPopupLayer", "visibility", "hidden")
session.updateInnerHTML("ruiPopupLayer", "")
} else { } else {
manager.popups = manager.popups[:count-1] manager.popups = manager.popups[:count-1]
manager.updatePopupLayerInnerHTML(session)
} }
popup.onDismiss() } else if index == 0 {
return
}
for n, p := range manager.popups {
if p == popup {
if n == 0 {
manager.popups = manager.popups[1:] manager.popups = manager.popups[1:]
} else { } else {
manager.popups = append(manager.popups[:n], manager.popups[n+1:]...) manager.popups = append(manager.popups[:index], manager.popups[index+1:]...)
} }
manager.updatePopupLayerInnerHTML(session)
popup.onDismiss() popup.onDismiss()
return
} }
if !popup.dissmissAnimation(listener) {
listener("")
} }
} }

View File

@ -534,6 +534,10 @@ func (layout *stackLayoutData) moveToFrontByIndex(index int, onShow []func(View)
layout.views = append(append(layout.views[:index], layout.views[index+1:]...), view) layout.views = append(append(layout.views[:index], layout.views[index+1:]...), view)
} }
if !layout.created {
return
}
session := layout.Session() session := layout.Session()
pageID := view.htmlID() + "page" pageID := view.htmlID() + "page"
peekPageID := peekID + "page" peekPageID := peekID + "page"
@ -649,6 +653,7 @@ func (layout *stackLayoutData) Append(view View) {
layout.views = append(layout.views, view) layout.views = append(layout.views, view)
} }
if layout.created {
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
@ -667,6 +672,7 @@ func (layout *stackLayoutData) Append(view View) {
if listener, ok := layout.changeListener[Content]; ok { if listener, ok := layout.changeListener[Content]; ok {
listener(layout, Content) listener(layout, Content)
} }
}
} }
// Insert inserts a view to the "index" position in the list of a view children // Insert inserts a view to the "index" position in the list of a view children

View File

@ -712,7 +712,7 @@ func (style *viewStyle) writeViewTransformCSS(builder cssBuilder, session Sessio
} }
if transform := getTransformProperty(style, Transform); transform != nil { if transform := getTransformProperty(style, Transform); transform != nil {
builder.add(`TransformProperty`, transform.transformCSS(session)) builder.add(`transform`, transform.transformCSS(session))
} }
} }

View File

@ -75,6 +75,7 @@ func (container *viewsContainerData) Append(view View) {
container.views = append(container.views, view) container.views = append(container.views, view)
} }
if container.created {
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
@ -85,6 +86,7 @@ func (container *viewsContainerData) Append(view View) {
listener(container, Content) listener(container, Content)
} }
} }
}
} }
// Insert inserts a view to the "index" position in the list of a view children // Insert inserts a view to the "index" position in the list of a view children
@ -102,11 +104,14 @@ func (container *viewsContainerData) Insert(view View, index int) {
} else { } else {
container.views = append([]View{view}, container.views...) container.views = append([]View{view}, container.views...)
} }
if container.created {
updateInnerHTML(htmlID, container.Session()) updateInnerHTML(htmlID, container.Session())
if listener, ok := container.changeListener[Content]; ok { if listener, ok := container.changeListener[Content]; ok {
listener(container, Content) listener(container, Content)
} }
} }
}
} }
// Remove removes view from list and return it // Remove removes view from list and return it
@ -132,10 +137,12 @@ func (container *viewsContainerData) RemoveView(index int) View {
view.setParentID("") view.setParentID("")
if container.created {
container.Session().callFunc("removeView", view.htmlID()) container.Session().callFunc("removeView", view.htmlID())
if listener, ok := container.changeListener[Content]; ok { if listener, ok := container.changeListener[Content]; ok {
listener(container, Content) listener(container, Content)
} }
}
return view return view
} }