diff --git a/CHANGELOG.md b/CHANGELOG.md index 615a4ba..4345e6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,13 @@ # v0.18.0 -* Property name type changed to PropertyName. +* Property name type changed from string to PropertyName. * Transform interface renamed to TransformProperty. NewTransform function renamed to NewTransformProperty. TransformTag constant renamed to Transform. * OriginX, OriginY, and OriginZ properties renamed to TransformOriginX, TransformOriginY, and TransformOriginZ -* GetOrigin function renamed to GetTransformOrigin +* GetOrigin function renamed to GetTransformOrigin. +* Changed Push and Pop method of StackLayout interface. +* Removed DefaultAnimation, StartToEndAnimation, EndToStartAnimation, TopDownAnimation, and BottomUpAnimation constants. +* Added "push-transform", "push-duration", "push-timing", and "move-to-front-animation" properties. +* Added GetPushDuration, GetPushTiming, and IsMoveToFrontAnimation functions. * Added LineJoin type. Type of constants MiterJoin, RoundJoin, and BevelJoin changed to LineJoin. Type of Canvas.SetLineJoin function argument changed to LineJoin. * Added LineCap type. Type of constants ButtCap, RoundCap, and SquareCap changed to LineCap. Type of Canvas.SetLineCap function argument changed to LineCap. diff --git a/animation.go b/animation.go index e514816..e1012b2 100644 --- a/animation.go +++ b/animation.go @@ -556,7 +556,7 @@ func (animation *animationData) animationCSS(session Session) string { buffer.WriteString(" 1s ") } - buffer.WriteString(animation.timingFunctionCSS(session)) + buffer.WriteString(timingFunctionCSS(animation, TimingFunction, session)) if delay, ok := floatProperty(animation, Delay, session, 0); ok && delay > 0 { buffer.WriteString(fmt.Sprintf(" %gs", delay)) @@ -594,7 +594,7 @@ func (animation *animationData) transitionCSS(buffer *strings.Builder, session S buffer.WriteString(" 1s ") } - buffer.WriteString(animation.timingFunctionCSS(session)) + buffer.WriteString(timingFunctionCSS(animation, TimingFunction, session)) if delay, ok := floatProperty(animation, Delay, session, 0); ok && delay > 0 { buffer.WriteString(fmt.Sprintf(" %gs", delay)) @@ -643,8 +643,8 @@ func (animation *animationData) writeTransitionString(tag PropertyName, buffer * buffer.WriteString(" }") } -func (animation *animationData) timingFunctionCSS(session Session) string { - if timingFunction, ok := stringProperty(animation, TimingFunction, session); ok { +func timingFunctionCSS(properties Properties, tag PropertyName, session Session) string { + if timingFunction, ok := stringProperty(properties, tag, session); ok { if timingFunction, ok = session.resolveConstants(timingFunction); ok && isTimingFunctionValid(timingFunction) { return timingFunction } diff --git a/propertySet.go b/propertySet.go index 7b18683..028de6f 100644 --- a/propertySet.go +++ b/propertySet.go @@ -62,6 +62,7 @@ var boolProperties = []PropertyName{ Repeating, UserSelect, ColumnSpanAll, + MoveToFrontAnimation, } var intProperties = []PropertyName{ @@ -88,6 +89,7 @@ var floatProperties = map[PropertyName]struct{ min, max float64 }{ ProgressBarValue: {min: 0, max: math.MaxFloat64}, VideoWidth: {min: 0, max: 10000}, VideoHeight: {min: 0, max: 10000}, + PushDuration: {min: 0, max: math.MaxFloat64}, } var sizeProperties = map[PropertyName]string{ diff --git a/stackLayout.go b/stackLayout.go index eecff9f..b834002 100644 --- a/stackLayout.go +++ b/stackLayout.go @@ -2,22 +2,62 @@ package rui import ( "fmt" - "strconv" "strings" ) // Constants which represent [StackLayout] animation type during pushing or popping views const ( - // DefaultAnimation - default animation of StackLayout push - DefaultAnimation = 0 - // StartToEndAnimation - start to end animation of StackLayout push - StartToEndAnimation = 1 - // EndToStartAnimation - end to start animation of StackLayout push - EndToStartAnimation = 2 - // TopDownAnimation - top down animation of StackLayout push - TopDownAnimation = 3 - // BottomUpAnimation - bottom up animation of StackLayout push - BottomUpAnimation = 4 + // PushTransform is the constant for "push-transform" property tag. + // + // Used by `StackLayout`. + // Specify start translation, scale and rotation over x, y and z axes as well as a distortion + // for an animated pushing of a child view. + // + // 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}". + PushTransform = "push-transform" + + // PushDuration is the constant for "push-duration" property tag. + // + // Used by `StackLayout`. + // Sets the length of time in seconds that an push/pop animation takes to complete. + // + // Supported types: `float`, `int`, `string`. + // + // Internal type is `float`, other types converted to it during assignment. + PushDuration = "push-duration" + + // PushTiming is the constant for "push-timing" property tag. + // + // Used by `StackLayout`. + // Set how an push/pop 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. + PushTiming = "push-timing" + + // MoveToFrontAnimation is the constant for "move-to-front-animation" property tag. + // + // Used by `StackLayout`. + // Specifies whether animation is used when calling the MoveToFront/MoveToFrontByID method of StackLayout interface. + // + // Supported types: `bool`, `int`, `string`. + // + // Values: + // `true` or `1` or "true", "yes", "on", "1" - animation is used (default value). + // `false` or `0` or "false", "no", "off", "0" - animation is not used. + MoveToFrontAnimation = "move-to-front-animation" ) // StackLayout represents a StackLayout view @@ -45,21 +85,28 @@ type StackLayout interface { // * TopDownAnimation (3) - Top-down animation; // * BottomUpAnimation (4) - Bottom up animation. // The third argument `onPushFinished` is the function to be called when the animation ends. It may be nil. - Push(view View, animation int, onPushFinished func()) + Push(view View, onPushFinished func()) // Pop removes the current View from the container using animation. // The second argument `onPopFinished`` is the function to be called when the animation ends. It may be nil. // The function will return false if the StackLayout is empty and true if the current item has been removed. - Pop(animation int, onPopFinished func(View)) bool + Pop(onPopFinished func(View)) bool +} + +type pushFinished struct { + peekID string + listener func() +} + +type popFinished struct { + view View + listener func(View) } type stackLayoutData struct { viewsContainerData - peek, prevPeek int - pushView, popView View - animationType int - onPushFinished func() - onPopFinished func(View) + onPushFinished map[string]pushFinished + onPopFinished map[string]popFinished } // NewStackLayout create new StackLayout object and return it @@ -80,44 +127,88 @@ func (layout *stackLayoutData) init(session Session) { layout.viewsContainerData.init(session) layout.tag = "StackLayout" layout.systemClass = "ruiStackLayout" - layout.properties[TransitionEndEvent] = []func(View, PropertyName){layout.pushFinished, layout.popFinished} - layout.get = layout.getFunc + layout.onPushFinished = map[string]pushFinished{} + layout.onPopFinished = map[string]popFinished{} layout.set = layout.setFunc layout.remove = layout.removeFunc - layout.changed = layout.propertyChanged -} -func (layout *stackLayoutData) pushFinished(view View, tag PropertyName) { - if tag == "ruiPush" { - if layout.pushView != nil { - layout.pushView = nil - count := len(layout.views) - if count > 0 { - layout.peek = count - 1 - } else { - layout.peek = 0 - } - updateInnerHTML(layout.htmlID(), layout.session) - layout.currentChanged() - } - - if layout.onPushFinished != nil { - onPushFinished := layout.onPushFinished - layout.onPushFinished = nil - onPushFinished() - } + layout.setRaw(TransitionEndEvent, []func(View, PropertyName){layout.transitionFinished}) + if session.TextDirection() == RightToLeftDirection { + layout.setRaw(PushTransform, NewTransformProperty(Params{TranslateX: Percent(-100)})) + } else { + layout.setRaw(PushTransform, NewTransformProperty(Params{TranslateX: Percent(100)})) } } -func (layout *stackLayoutData) popFinished(view View, tag PropertyName) { - if tag == "ruiPop" { - popView := layout.popView - layout.popView = nil - updateInnerHTML(layout.htmlID(), layout.session) - if layout.onPopFinished != nil { - onPopFinished := layout.onPopFinished - layout.onPopFinished = nil - onPopFinished(popView) +func (layout *stackLayoutData) transitionFinished(view View, tag PropertyName) { + if tags := strings.Split(string(tag), "-"); len(tags) >= 2 { + session := layout.Session() + viewID := tags[1] + + switch tags[0] { + case "push": + if finished, ok := layout.onPushFinished[viewID]; ok { + if finished.peekID != "" { + pageID := finished.peekID + "page" + session.startUpdateScript(pageID) + session.updateCSSProperty(pageID, "visibility", "hidden") + session.updateCSSProperty(pageID, "transition", "") + session.updateCSSProperty(pageID, "transform", "") + session.removeProperty(pageID, "ontransitionend") + session.removeProperty(pageID, "ontransitioncancel") + session.finishUpdateScript(pageID) + } + + pageID := viewID + "page" + session.startUpdateScript(pageID) + session.updateCSSProperty(pageID, "z-index", "auto") + session.updateCSSProperty(pageID, "transition", "") + session.removeProperty(pageID, "ontransitionend") + session.removeProperty(pageID, "ontransitioncancel") + session.finishUpdateScript(pageID) + + if finished.listener != nil { + finished.listener() + } + delete(layout.onPushFinished, viewID) + layout.contentChanged() + } + + case "pop": + if finished, ok := layout.onPopFinished[viewID]; ok { + session.updateCSSProperty(viewID+"page", "display", "none") + if finished.listener != nil { + finished.listener(finished.view) + } + delete(layout.onPopFinished, viewID) + + if count := len(layout.views); count > 0 { + peekID := layout.views[count-1].htmlID() + "page" + session.startUpdateScript(peekID) + session.removeProperty(peekID, "ontransitionend") + session.removeProperty(peekID, "ontransitioncancel") + session.finishUpdateScript(peekID) + } + } + + case "move": + if count := len(layout.views); count > 1 { + pageID := layout.views[count-2].htmlID() + "page" + session.startUpdateScript(pageID) + session.updateCSSProperty(pageID, "visibility", "hidden") + session.updateCSSProperty(pageID, "transition", "") + session.updateCSSProperty(pageID, "transform", "") + session.finishUpdateScript(pageID) + } + + pageID := viewID + "page" + session.startUpdateScript(pageID) + session.updateCSSProperty(pageID, "z-index", "auto") + session.updateCSSProperty(pageID, "transition", "") + session.removeProperty(pageID, "ontransitionend") + session.removeProperty(pageID, "ontransitioncancel") + session.finishUpdateScript(pageID) + layout.contentChanged() } } } @@ -125,199 +216,225 @@ func (layout *stackLayoutData) popFinished(view View, tag PropertyName) { func (layout *stackLayoutData) setFunc(tag PropertyName, value any) []PropertyName { switch tag { case TransitionEndEvent: + // TODO listeners, ok := valueToOneArgEventListeners[View, PropertyName](value) if ok && listeners != nil { - listeners = append(listeners, layout.pushFinished) - listeners = append(listeners, layout.popFinished) + listeners = append(listeners, layout.transitionFinished) layout.setRaw(TransitionEndEvent, listeners) return []PropertyName{tag} } return nil - case Current: - newCurrent := 0 - switch value := value.(type) { - case string: - text, ok := layout.session.resolveConstants(value) - if !ok { - invalidPropertyValue(tag, value) - return nil - } - n, err := strconv.Atoi(strings.Trim(text, " \t")) - if err != nil { - invalidPropertyValue(tag, value) - ErrorLog(err.Error()) - return nil - } - newCurrent = n - - default: - n, ok := isInt(value) - if !ok { - notCompatibleType(tag, value) - return nil - } else if n < 0 || n >= len(layout.views) { - ErrorLogF(`The view index "%d" of "%s" property is out of range`, n, tag) - return nil - } - newCurrent = n + case PushTiming: + if text, ok := value.(string); ok { + layout.setRaw(tag, text) + return []PropertyName{tag} } - - layout.prevPeek = layout.peek - if newCurrent == layout.peek { - return []PropertyName{} - } - - layout.peek = newCurrent - return []PropertyName{tag} } return layout.viewsContainerData.setFunc(tag, value) } -func (layout *stackLayoutData) propertyChanged(tag PropertyName) { - switch tag { - case Current: - if layout.prevPeek != layout.peek { - if layout.prevPeek < len(layout.views) { - layout.Session().updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(layout.prevPeek), "visibility", "hidden") - } - layout.Session().updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(layout.peek), "visibility", "visible") - layout.prevPeek = layout.peek - } - default: - layout.viewsContainerData.propertyChanged(tag) - } -} - func (layout *stackLayoutData) removeFunc(tag PropertyName) []PropertyName { switch tag { case TransitionEndEvent: - layout.setRaw(TransitionEndEvent, []func(View, PropertyName){layout.pushFinished, layout.popFinished}) - return []PropertyName{tag} - - case Current: - layout.setRaw(Current, 0) + layout.setRaw(TransitionEndEvent, []func(View, PropertyName){layout.transitionFinished}) return []PropertyName{tag} } return layout.viewsContainerData.removeFunc(tag) } -func (layout *stackLayoutData) getFunc(tag PropertyName) any { - if tag == Current { - return layout.peek - } - return layout.viewsContainerData.getFunc(tag) -} - func (layout *stackLayoutData) Peek() View { - if int(layout.peek) < len(layout.views) { - return layout.views[layout.peek] + if count := len(layout.views); count > 0 { + return layout.views[count-1] } return nil } func (layout *stackLayoutData) MoveToFront(view View) bool { - peek := int(layout.peek) - htmlID := view.htmlID() - for i, view2 := range layout.views { - if view2.htmlID() == htmlID { - if i != peek { - if peek < len(layout.views) { - layout.Session().updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(peek), "visibility", "hidden") - } + if view == nil { + ErrorLog(`MoveToFront(nil) forbidden`) + return false + } - layout.peek = i - layout.Session().updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(i), "visibility", "visible") - layout.currentChanged() - } + htmlID := view.htmlID() + switch count := len(layout.views); count { + case 0: + // do nothing + + case 1: + if layout.views[0].htmlID() == htmlID { return true } + + default: + for i, view := range layout.views { + if view.htmlID() == htmlID { + layout.moveToFrontByIndex(i) + return true + } + } } - ErrorLog(`MoveToFront() fail. Subview not found."`) + ErrorLog(`MoveToFront() fail. Subview not found.`) return false } -func (layout *stackLayoutData) currentChanged() { - if listener, ok := layout.changeListener[Current]; ok { - listener(layout, Current) - } -} - func (layout *stackLayoutData) MoveToFrontByID(viewID string) bool { - peek := int(layout.peek) - for i, view := range layout.views { - if view.ID() == viewID { - if i != peek { - if peek < len(layout.views) { - layout.Session().updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(peek), "visibility", "hidden") - } + switch count := len(layout.views); count { + case 0: + // do nothing - layout.peek = i - layout.Session().updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(i), "visibility", "visible") - layout.currentChanged() - } + case 1: + if layout.views[0].ID() == viewID { return true } + + default: + for i, view := range layout.views { + if view.ID() == viewID { + layout.moveToFrontByIndex(i) + return true + } + } } - ErrorLogF(`MoveToFront("%s") fail. Subview with "%s" not found."`, viewID, viewID) + ErrorLogF(`MoveToFront("%s") fail. Subview with "%s" not found.`, viewID, viewID) return false } -func (layout *stackLayoutData) Append(view View) { - if view != nil { - layout.peek = len(layout.views) - layout.viewsContainerData.Append(view) - layout.currentChanged() - } else { - ErrorLog("StackLayout.Append(nil, ....) is forbidden") +func (layout *stackLayoutData) moveToFrontByIndex(index int) { + + count := len(layout.views) + if index == count-1 { + return } + + view := layout.views[index] + peekID := layout.views[count-1].htmlID() + if index == 0 { + layout.views = append(layout.views[1:], view) + } else { + layout.views = append(append(layout.views[:index], layout.views[index+1:]...), view) + } + + session := layout.Session() + pageID := view.htmlID() + "page" + peekPageID := peekID + "page" + + animated := IsMoveToFrontAnimation(layout) + + var transform TransformProperty = nil + if animated { + transform = GetPushTransform(layout) + } + + if transform == nil { + session.updateCSSProperty(peekPageID, "visibility", "hidden") + session.updateCSSProperty(pageID, "visibility", "visible") + layout.contentChanged() + return + } + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + buffer.WriteString(`stackTransitionEndEvent('`) + buffer.WriteString(layout.htmlID()) + buffer.WriteString(`', 'move-`) + buffer.WriteString(view.htmlID()) + buffer.WriteString(`', event)`) + + listener := buffer.String() + + transformCSS := transformMirror(transform, session).transformCSS(session) + transitionCSS := layout.pushTransitionCSS() + + session.updateCSSProperty(peekPageID, "transition", transitionCSS) + + session.startUpdateScript(pageID) + session.updateProperty(pageID, "ontransitionend", listener) + session.updateProperty(pageID, "ontransitioncancel", listener) + session.updateCSSProperty(pageID, "transform", transformCSS) + session.updateCSSProperty(pageID, "z-index", "100") + session.updateCSSProperty(pageID, "visibility", "visible") + session.finishUpdateScript(pageID) + + session.updateCSSProperty(pageID, "transition", transitionCSS) + session.updateCSSProperty(pageID, "transform", "") + + session.updateCSSProperty(peekPageID, "transform", transformCSS) } -func (layout *stackLayoutData) Insert(view View, index int) { - if view != nil { - count := len(layout.views) - if index < count { - layout.peek = int(index) - } else { - layout.peek = count - } - layout.viewsContainerData.Insert(view, index) - layout.currentChanged() - } else { - ErrorLog("StackLayout.Insert(nil, ....) is forbidden") +func (layout *stackLayoutData) contentChanged() { + if listener, ok := layout.changeListener[Content]; ok { + listener(layout, Content) } } -func (layout *stackLayoutData) RemoveView(index int) View { - if index < 0 || index >= len(layout.views) { - return nil - } - - if layout.peek > 0 { - layout.peek-- - } - defer layout.currentChanged() - return layout.viewsContainerData.RemoveView(index) -} - func (layout *stackLayoutData) RemovePeek() View { return layout.RemoveView(len(layout.views) - 1) } -func (layout *stackLayoutData) Push(view View, animation int, onPushFinished func()) { +func (layout *stackLayoutData) pushTransitionCSS() string { + return fmt.Sprintf("transform %.2fs %s", GetPushDuration(layout), GetPushTiming(layout)) +} + +func transformMirror(transform TransformProperty, session Session) TransformProperty { + result := NewTransformProperty(nil) + + for _, tag := range []PropertyName{Perspective, RotateX, RotateY, RotateZ, ScaleX, ScaleY, ScaleZ, TranslateZ} { + if value := transform.getRaw(tag); value != nil { + result.Set(tag, value) + } + } + + for _, tag := range []PropertyName{Rotate, SkewX, SkewY} { + if angle, ok := angleProperty(transform, tag, session); ok { + angle.Value = -angle.Value + result.Set(tag, angle) + } + } + + for _, tag := range []PropertyName{TranslateX, TranslateY} { + if size, ok := sizeProperty(transform, tag, session); ok { + size.Value = -size.Value + result.Set(tag, size) + } + } + + return result +} + +func (layout *stackLayoutData) Push(view View, onPushFinished func()) { if view == nil { ErrorLog("StackLayout.Push(nil, ....) is forbidden") return } - layout.pushView = view - layout.animationType = animation - //layout.animation["ruiPush"] = Animation{FinishListener: layout} - layout.onPushFinished = onPushFinished + transform := GetPushTransform(layout) + if transform == nil { + layout.Append(view) + if onPushFinished != nil { + onPushFinished() + } + return + } + + prevPeek := "" + finished := pushFinished{ + listener: onPushFinished, + } + if count := len(layout.views); count > 0 { + finished.peekID = layout.views[count-1].htmlID() + prevPeek = finished.peekID + "page" + } + + htmlID := view.htmlID() + layout.onPushFinished[htmlID] = finished + + view.setParentID(layout.htmlID()) + layout.views = append(layout.views, view) - htmlID := layout.htmlID() session := layout.Session() buffer := allocStringBuilder() @@ -325,105 +442,110 @@ func (layout *stackLayoutData) Push(view View, animation int, onPushFinished fun buffer.WriteString(`