package rui import ( "fmt" "strings" ) // Constants which represent [StackLayout] animation type during pushing or popping views const ( // 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 type StackLayout interface { ViewsContainer // Peek returns the current (visible) View. If StackLayout is empty then it returns nil. Peek() View // RemovePeek removes the current View and returns it. If StackLayout is empty then it doesn't do anything and returns nil. RemovePeek() View // MoveToFront makes the given View current. Returns true if successful, false otherwise. MoveToFront(view View) bool // MoveToFrontByID makes the View current by viewID. Returns true if successful, false otherwise. MoveToFrontByID(viewID string) bool // Push adds a new View to the container and makes it current. // It is similar to Append, but the addition is done using an animation effect. // The animation type is specified by the second argument and can take the following values: // * DefaultAnimation (0) - Default animation. For the Push function it is EndToStartAnimation, for Pop - StartToEndAnimation; // * StartToEndAnimation (1) - Animation from beginning to end. The beginning and the end are determined by the direction of the text output; // * EndToStartAnimation (2) - End-to-Beginning animation; // * 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, 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(onPopFinished func(View)) bool } type pushFinished struct { peekID string listener func() } type popFinished struct { view View listener func(View) } type stackLayoutData struct { viewsContainerData onPushFinished map[string]pushFinished onPopFinished map[string]popFinished } // NewStackLayout create new StackLayout object and return it func NewStackLayout(session Session, params Params) StackLayout { view := new(stackLayoutData) view.init(session) setInitParams(view, params) return view } func newStackLayout(session Session) View { //return NewStackLayout(session, nil) return new(stackLayoutData) } // Init initialize fields of ViewsContainer by default values func (layout *stackLayoutData) init(session Session) { layout.viewsContainerData.init(session) layout.tag = "StackLayout" layout.systemClass = "ruiStackLayout" layout.onPushFinished = map[string]pushFinished{} layout.onPopFinished = map[string]popFinished{} layout.set = layout.setFunc layout.remove = layout.removeFunc 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) 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() } } } 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.transitionFinished) layout.setRaw(TransitionEndEvent, listeners) return []PropertyName{tag} } return nil case PushTiming: if text, ok := value.(string); ok { layout.setRaw(tag, text) return []PropertyName{tag} } } return layout.viewsContainerData.setFunc(tag, value) } func (layout *stackLayoutData) removeFunc(tag PropertyName) []PropertyName { switch tag { case TransitionEndEvent: layout.setRaw(TransitionEndEvent, []func(View, PropertyName){layout.transitionFinished}) return []PropertyName{tag} } return layout.viewsContainerData.removeFunc(tag) } func (layout *stackLayoutData) Peek() View { if count := len(layout.views); count > 0 { return layout.views[count-1] } return nil } func (layout *stackLayoutData) MoveToFront(view View) bool { if view == nil { ErrorLog(`MoveToFront(nil) forbidden`) return false } 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.`) return false } func (layout *stackLayoutData) MoveToFrontByID(viewID string) bool { switch count := len(layout.views); count { case 0: // do nothing 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) return false } 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) contentChanged() { if listener, ok := layout.changeListener[Content]; ok { listener(layout, Content) } } func (layout *stackLayoutData) RemovePeek() View { return layout.RemoveView(len(layout.views) - 1) } 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 } 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) session := layout.Session() buffer := allocStringBuilder() defer freeStringBuilder(buffer) buffer.WriteString(`