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 {
background-color: rgba(128,128,128,0.1);
/*background-color: rgba(128,128,128,0.1);*/
position: absolute;
top: 0px;
bottom: 0px;

View File

@ -11,6 +11,14 @@ import (
// Color - represent color in argb format
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
func (color Color) ARGB() (uint8, uint8, uint8, uint8) {
return uint8(color >> 24),

298
popup.go
View File

@ -1,6 +1,7 @@
package rui
import (
"fmt"
"strings"
)
@ -143,6 +144,48 @@ const (
// See `SizeUnit` description for more details.
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.
//
// Used by `Popup`.
@ -219,14 +262,21 @@ type Popup interface {
html(buffer *strings.Builder)
viewByHTMLID(id string) View
keyEvent(event KeyEvent) bool
showAnimation()
dissmissAnimation(listener func(PropertyName)) bool
}
type popupData struct {
layerView View
view View
layerView GridLayout
popupView GridLayout
contentView View
buttons []PopupButton
cancelable bool
dismissListener []func(Popup)
showTransform TransformProperty
showOpacity float64
showDuration float64
showTiming string
}
type popupManager struct {
@ -397,39 +447,15 @@ func (arrow *popupArrow) createView(popupView View) View {
return NewGridLayout(session, params)
}
func (popup *popupData) init(view View, popupParams Params) {
popup.view = view
popup.cancelable = false
session := view.Session()
func (popup *popupData) layerCellWidth(arrowLocation int, popupParams Params, session Session) []SizeUnit {
columnCount := 3
rowCount := 3
popupRow := 1
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:
var columnCount int
switch arrowLocation {
case LeftArrow, RightArrow:
columnCount = 4
popupColumn = 2
case RightArrow:
columnCount = 4
arrow.column = 2
default:
columnCount = 3
}
cellWidth := make([]SizeUnit, columnCount)
@ -444,6 +470,19 @@ func (popup *popupData) init(view View, popupParams Params) {
cellWidth[0] = 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)
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)
}
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{
Style: "ruiPopupLayer",
MaxWidth: Percent(100),
MaxHeight: Percent(100),
CellWidth: cellWidth,
CellHeight: cellHeight,
CellWidth: popup.layerCellWidth(arrow.location, popupParams, session),
CellHeight: popup.layerCellHeight(arrow.location, popupParams, session),
}
params := Params{
Style: "ruiPopup",
ID: "ruiPopup",
Row: popupRow,
Column: popupColumn,
MaxWidth: Percent(100),
@ -483,10 +553,14 @@ func (popup *popupData) init(view View, popupParams Params) {
}
var closeButton View = nil
outsideClose := false
buttons := []PopupButton{}
titleStyle := "ruiPopupTitle"
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 {
if value != nil {
@ -532,10 +606,10 @@ func (popup *popupData) init(view View, popupParams Params) {
case Buttons:
switch value := value.(type) {
case PopupButton:
buttons = []PopupButton{value}
popup.buttons = []PopupButton{value}
case []PopupButton:
buttons = value
popup.buttons = value
}
case Title:
@ -587,13 +661,36 @@ func (popup *popupData) init(view View, popupParams Params) {
case ArrowOffset:
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:
params[tag] = value
}
}
}
popupView := NewGridLayout(view.Session(), params)
popup.popupView = NewGridLayout(view.Session(), params)
var popupCellHeight []SizeUnit
viewRow := 0
@ -605,7 +702,7 @@ func (popup *popupData) init(view View, popupParams Params) {
if closeButton != nil {
titleContent = append(titleContent, closeButton)
}
popupView.Append(NewGridLayout(session, Params{
popup.popupView.Append(NewGridLayout(session, Params{
Row: 0,
Style: titleStyle,
CellWidth: []any{Fr(1), AutoSize()},
@ -621,10 +718,9 @@ func (popup *popupData) init(view View, popupParams Params) {
}
view.Set(Row, viewRow)
popupView.Append(view)
popup.popupView.Append(view)
popup.buttons = buttons
if buttonCount := len(buttons); buttonCount > 0 {
if buttonCount := len(popup.buttons); buttonCount > 0 {
buttonsAlign, _ := enumProperty(params, ButtonsAlign, session, RightAlign)
popupCellHeight = append(popupCellHeight, AutoSize())
gap, _ := sizeConstant(session, "ruiPopupButtonGap")
@ -641,7 +737,7 @@ func (popup *popupData) init(view View, popupParams Params) {
buttonsPanel.Set(Margin, gap)
}
for i, button := range buttons {
for i, button := range popup.buttons {
title := button.Title
if title == "" && button.Type == CancelButton {
title = "Cancel"
@ -668,33 +764,82 @@ func (popup *popupData) init(view View, popupParams Params) {
buttonsPanel.Append(buttonView)
}
popupView.Append(NewGridLayout(session, Params{
popup.popupView.Append(NewGridLayout(session, Params{
Row: viewRow + 1,
CellHorizontalAlign: buttonsAlign,
Content: buttonsPanel,
}))
}
popupView.Set(CellHeight, popupCellHeight)
popup.popupView.Set(CellHeight, popupCellHeight)
if arrow.location != NoneArrow {
layerParams[Content] = []View{popupView, arrow.createView(popupView)}
layerParams[Content] = []View{popup.popupView, arrow.createView(popup.popupView)}
} else {
layerParams[Content] = []View{popupView}
layerParams[Content] = []View{popup.popupView}
}
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 {
popup.layerView.Set(ClickEvent, popup.cancel)
}
}
func (popup popupData) View() View {
return popup.view
func (popup *popupData) showAnimation() {
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 {
return popup.view.Session()
return popup.contentView.Session()
}
func (popup *popupData) cancel() {
@ -709,9 +854,6 @@ func (popup *popupData) cancel() {
func (popup *popupData) Dismiss() {
popup.Session().popupManager().dismissPopup(popup)
for _, listener := range popup.dismissListener {
listener(popup)
}
}
func (popup *popupData) Show() {
@ -719,7 +861,6 @@ func (popup *popupData) Show() {
}
func (popup *popupData) html(buffer *strings.Builder) {
viewHTML(popup.layerView, buffer, "")
}
@ -728,6 +869,8 @@ func (popup *popupData) viewByHTMLID(id string) View {
}
func (popup *popupData) onDismiss() {
popup.Session().callFunc("removeView", popup.layerView.htmlID())
for _, listener := range popup.dismissListener {
listener(popup)
}
@ -808,6 +951,7 @@ func (manager *popupManager) showPopup(popup Popup) {
session.updateCSSProperty("ruiTooltipLayer", "opacity", "0")
session.updateCSSProperty("ruiPopupLayer", "visibility", "visible")
session.updateCSSProperty("ruiRoot", "pointer-events", "none")
popup.showAnimation()
}
func (manager *popupManager) dismissPopup(popup Popup) {
@ -821,31 +965,37 @@ func (manager *popupManager) dismissPopup(popup Popup) {
return
}
session := popup.Session()
if manager.popups[count-1] == popup {
if count == 1 {
manager.popups = []Popup{}
session.updateCSSProperty("ruiRoot", "pointer-events", "auto")
session.updateCSSProperty("ruiPopupLayer", "visibility", "hidden")
session.updateInnerHTML("ruiPopupLayer", "")
} else {
manager.popups = manager.popups[:count-1]
manager.updatePopupLayerInnerHTML(session)
index := -1
for n, p := range manager.popups {
if p == popup {
index = n
break
}
popup.onDismiss()
}
if index < 0 {
return
}
for n, p := range manager.popups {
if p == popup {
if n == 0 {
manager.popups = manager.popups[1:]
session := popup.Session()
listener := func(PropertyName) {
if index == count-1 {
if count == 1 {
manager.popups = []Popup{}
session.updateCSSProperty("ruiRoot", "pointer-events", "auto")
session.updateCSSProperty("ruiPopupLayer", "visibility", "hidden")
} else {
manager.popups = append(manager.popups[:n], manager.popups[n+1:]...)
manager.popups = manager.popups[:count-1]
}
manager.updatePopupLayerInnerHTML(session)
popup.onDismiss()
return
} else if index == 0 {
manager.popups = manager.popups[1:]
} else {
manager.popups = append(manager.popups[:index], manager.popups[index+1:]...)
}
popup.onDismiss()
}
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)
}
if !layout.created {
return
}
session := layout.Session()
pageID := view.htmlID() + "page"
peekPageID := peekID + "page"
@ -649,23 +653,25 @@ func (layout *stackLayoutData) Append(view View) {
layout.views = append(layout.views, view)
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if layout.created {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`<div id="`)
buffer.WriteString(view.htmlID())
buffer.WriteString(`page" class="ruiStackPageLayout">`)
viewHTML(view, buffer, "")
buffer.WriteString(`</div>`)
buffer.WriteString(`<div id="`)
buffer.WriteString(view.htmlID())
buffer.WriteString(`page" class="ruiStackPageLayout">`)
viewHTML(view, buffer, "")
buffer.WriteString(`</div>`)
session := layout.Session()
if count > 0 {
session.updateCSSProperty(layout.views[count-1].htmlID()+"page", "visibility", "hidden")
}
session.appendToInnerHTML(stackID, buffer.String())
session := layout.Session()
if count > 0 {
session.updateCSSProperty(layout.views[count-1].htmlID()+"page", "visibility", "hidden")
}
session.appendToInnerHTML(stackID, buffer.String())
if listener, ok := layout.changeListener[Content]; ok {
listener(layout, Content)
if listener, ok := layout.changeListener[Content]; ok {
listener(layout, Content)
}
}
}

View File

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

View File

@ -75,14 +75,16 @@ func (container *viewsContainerData) Append(view View) {
container.views = append(container.views, view)
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if container.created {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
viewHTML(view, buffer, "")
container.Session().appendToInnerHTML(htmlID, buffer.String())
viewHTML(view, buffer, "")
container.Session().appendToInnerHTML(htmlID, buffer.String())
if listener, ok := container.changeListener[Content]; ok {
listener(container, Content)
if listener, ok := container.changeListener[Content]; ok {
listener(container, Content)
}
}
}
}
@ -102,9 +104,12 @@ func (container *viewsContainerData) Insert(view View, index int) {
} else {
container.views = append([]View{view}, container.views...)
}
updateInnerHTML(htmlID, container.Session())
if listener, ok := container.changeListener[Content]; ok {
listener(container, Content)
if container.created {
updateInnerHTML(htmlID, container.Session())
if listener, ok := container.changeListener[Content]; ok {
listener(container, Content)
}
}
}
}
@ -132,9 +137,11 @@ func (container *viewsContainerData) RemoveView(index int) View {
view.setParentID("")
container.Session().callFunc("removeView", view.htmlID())
if listener, ok := container.changeListener[Content]; ok {
listener(container, Content)
if container.created {
container.Session().callFunc("removeView", view.htmlID())
if listener, ok := container.changeListener[Content]; ok {
listener(container, Content)
}
}
return view
}