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" // PushPerspective is the constant for "push-perspective" property tag. // // Used by `StackLayout`. // // Used to access the "perspective" property of StackLayout "push-transform" property: // Distance between the z-plane and the user in order to give a 3D-positioned element some perspective. Each 3D element // with z > 0 becomes larger, each 3D-element with z < 0 becomes smaller. The default value is 0 (no 3D effects). // // Supported types: `SizeUnit`, `SizeFunc`, `string`, `float`, `int`. // // Internal type is `SizeUnit`, other types converted to it during assignment. // See `SizeUnit` description for more details. PushPerspective PropertyName = "push-perspective" // PushTranslateX is the constant for "push-translate-x" property tag. // // Used by `StackLayout`. // // Used to access the "translate-x" property of StackLayout "push-transform" property: // x-axis translation value of a 2D/3D translation. // // Supported types: `SizeUnit`, `SizeFunc`, `string`, `float`, `int`. // // Internal type is `SizeUnit`, other types converted to it during assignment. // See `SizeUnit` description for more details. PushTranslateX PropertyName = "push-translate-x" // PushTranslateY is the constant for "push-translate-y" property tag. // // Used by `StackLayout`. // // Used to access the "translate-y" property of StackLayout "push-transform" property: // y-axis translation value of a 2D/3D translation. // // Supported types: `SizeUnit`, `SizeFunc`, `string`, `float`, `int`. // // Internal type is `SizeUnit`, other types converted to it during assignment. // See `SizeUnit` description for more details. PushTranslateY PropertyName = "push-translate-y" // PushTranslateZ is the constant for "push-translate-z" property tag. // // Used by `StackLayout`. // // Used to access the "translate-z" property of StackLayout "push-transform" property: // z-axis translation value of a 3D translation. // // Supported types: `SizeUnit`, `SizeFunc`, `string`, `float`, `int`. // // Internal type is `SizeUnit`, other types converted to it during assignment. // See `SizeUnit` description for more details. PushTranslateZ PropertyName = "push-translate-z" // PushScaleX is the constant for "push-scale-x" property tag. // // Used by `StackLayout`. // // Used to access the "scale-x" property of StackLayout "push-transform" property: // x-axis scaling value of a 2D/3D scale. The original scale is 1. Values between 0 and 1 are used to decrease original // scale, more than 1 - to increase. The default value is 1. // // Supported types: `float`, `int`, `string`. // // Internal type is `float`, other types converted to it during assignment. PushScaleX PropertyName = "push-scale-x" // PushScaleY is the constant for "push-scale-y" property tag. // // Used by `StackLayout`. // // Used to access the "scale-y" property of StackLayout "push-transform" property: // y-axis scaling value of a 2D/3D scale. The original scale is 1. Values between 0 and 1 are used to decrease original // scale, more than 1 - to increase. The default value is 1. // // Supported types: `float`, `int`, `string`. // // Internal type is `float`, other types converted to it during assignment. PushScaleY PropertyName = "push-scale-y" // PushScaleZ is the constant for "push-scale-z" property tag. // // Used by `StackLayout`. // // Used to access the "scale-z" property of StackLayout "push-transform" property: // z-axis scaling value of a 3D scale. The original scale is 1. Values between 0 and 1 are used to decrease original // scale, more than 1 - to increase. The default value is 1. // // Supported types: `float`, `int`, `string`. // // Internal type is `float`, other types converted to it during assignment. PushScaleZ PropertyName = "push-scale-z" // PushRotate is the constant for "push-rotate" property tag. // // Used by `StackLayout`. // // Used to access the "rotate" property of StackLayout "push-transform" property: // Angle of the view rotation. A positive angle denotes a clockwise rotation, a negative angle a counter-clockwise. // // Supported types: `AngleUnit`, `string`, `float`, `int`. // // Internal type is `AngleUnit`, other types will be converted to it during assignment. // See `AngleUnit` description for more details. // // Conversion rules: // `AngleUnit` - stored as is, no conversion performed. // `string` - must contain string representation of `AngleUnit`. If numeric value will be provided without any suffix then `AngleUnit` with value and `Radian` value type will be created. // `float` - a new `AngleUnit` value will be created with `Radian` as a type. // `int` - a new `AngleUnit` value will be created with `Radian` as a type. PushRotate PropertyName = "push-rotate" // PushRotateX is the constant for "push-rotate-x" property tag. // // Used by `StackLayout`. // // Used to access the "rotate-x" property of StackLayout "push-transform" property: // x-coordinate of the vector denoting the axis of rotation in range 0 to 1. // // Supported types: `float`, `int`, `string`. // // Internal type is `float`, other types converted to it during assignment. PushRotateX PropertyName = "push-rotate-x" // PushRotateY is the constant for "push-rotate-y" property tag. // // Used by `StackLayout`. // // Used to access the "rotate-y" property of StackLayout "push-transform" property: // y-coordinate of the vector denoting the axis of rotation in range 0 to 1. // // Supported types: `float`, `int`, `string`. // // Internal type is `float`, other types converted to it during assignment. PushRotateY PropertyName = "push-rotate-y" // PushRotateZ is the constant for "push-rotate-z" property tag. // // Used by `StackLayout`. // // Used to access the "rotate-z" property of StackLayout "push-transform" property: // z-coordinate of the vector denoting the axis of rotation in range 0 to 1. // // Supported types: `float`, `int`, `string`. // // Internal type is `float`, other types converted to it during assignment. PushRotateZ PropertyName = "push-rotate-z" // PushSkewX is the constant for "push-skew-x" property tag. // // Used by `StackLayout`. // // Used to access the "skew-x" property of StackLayout "push-transform" property: // Angle to use to distort the element along the abscissa. The default value is 0. // // Supported types: `AngleUnit`, `string`, `float`, `int`. // // Internal type is `AngleUnit`, other types will be converted to it during assignment. // See `AngleUnit` description for more details. // // Conversion rules: // `AngleUnit` - stored as is, no conversion performed. // `string` - must contain string representation of `AngleUnit`. If numeric value will be provided without any suffix then `AngleUnit` with value and `Radian` value type will be created. // `float` - a new `AngleUnit` value will be created with `Radian` as a type. // `int` - a new `AngleUnit` value will be created with `Radian` as a type. PushSkewX PropertyName = "push-skew-x" // PushSkewY is the constant for "push-skew-y" property tag. // // Used by `StackLayout`. // // Used to access the "skew-y" property of StackLayout "push-transform" property: // Angle to use to distort the element along the ordinate. The default value is 0. // // Supported types: `AngleUnit`, `string`, `float`, `int`. // // Internal type is `AngleUnit`, other types will be converted to it during assignment. // See `AngleUnit` description for more details. // // Conversion rules: // `AngleUnit` - stored as is, no conversion performed. // `string` - must contain string representation of `AngleUnit`. If numeric value will be provided without any suffix then `AngleUnit` with value and `Radian` value type will be created. // `float` - a new `AngleUnit` value will be created with `Radian` as a type. // `int` - a new `AngleUnit` value will be created with `Radian` as a type. PushSkewY PropertyName = "push-skew-y" ) // 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. // The second argument is a function called after the move to front animation ends. // Returns true if successful, false otherwise. MoveToFront(view View, onShown ...func(View)) bool // MoveToFrontByID makes the View current by viewID. // The second argument is a function called after the move to front animation ends. // Returns true if successful, false otherwise. MoveToFrontByID(viewID string, onShown ...func(View)) 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 second argument `onPushFinished` is the function to be called when the animation ends. Push(view View, onPushFinished ...func()) // Pop removes the current View from the container using animation. // The argument `onPopFinished` is the function to be called when the animation ends. // 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 onMoveFinished 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.onMoveFinished = map[string]popFinished{} layout.set = layout.setFunc layout.remove = layout.removeFunc layout.changed = layout.propertyChanged 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) for _, listener := range finished.listener { if listener != nil { listener() } } delete(layout.onPushFinished, viewID) layout.contentChanged() } case "pop": if finished, ok := layout.onPopFinished[viewID]; ok { session.callFunc("removeView", viewID+"page") for _, listener := range finished.listener { if listener != nil { 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() if finished, ok := layout.onMoveFinished[viewID]; ok { for _, listener := range finished.listener { if listener != nil { listener(finished.view) } } delete(layout.onMoveFinished, viewID) } } } } 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) propertyChanged(tag PropertyName) { switch tag { case PushTransform, PushTiming, PushDuration, MoveToFrontAnimation, PushPerspective, PushRotateX, PushRotateY, PushRotateZ, PushRotate, PushSkewX, PushSkewY, PushScaleX, PushScaleY, PushScaleZ, PushTranslateX, PushTranslateY, PushTranslateZ: // do nothing default: layout.viewsContainerData.propertyChanged(tag) } } 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, onShown ...func(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, onShown) return true } } } ErrorLog(`MoveToFront() fail. Subview not found.`) return false } func (layout *stackLayoutData) MoveToFrontByID(viewID string, onShown ...func(View)) 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, onShown) return true } } } ErrorLogF(`MoveToFront("%s") fail. Subview with "%s" not found.`, viewID, viewID) return false } func (layout *stackLayoutData) moveToFrontByIndex(index int, onShow []func(View)) { 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) } if !layout.created { return } 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() for _, listener := range onShow { if listener != nil { listener(view) } } return } layout.onMoveFinished[view.htmlID()] = popFinished{ view: view, listener: onShow, } 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 } // Append appends a view to the end of the list of a view children func (layout *stackLayoutData) Append(view View) { if view == nil { ErrorLog("StackLayout.Append(nil) is forbidden") return } stackID := layout.htmlID() view.setParentID(stackID) count := len(layout.views) if count == 0 { layout.views = []View{view} } else { layout.views = append(layout.views, view) } if layout.created { buffer := allocStringBuilder() defer freeStringBuilder(buffer) buffer.WriteString(`