2025-09-23 21:59:54 +08:00

773 lines
35 KiB
Swift

import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import GlassBackgroundComponent
import MultilineTextComponent
import LottieComponent
import UIKitRuntimeUtils
import BundleIconComponent
import TextBadgeComponent
public final class TabBarComponent: Component {
public final class Item: Equatable {
public let item: UITabBarItem
public let action: (Bool) -> Void
public let contextAction: ((ContextGesture, ContextExtractedContentContainingView) -> Void)?
fileprivate var id: AnyHashable {
return AnyHashable(ObjectIdentifier(self.item))
}
public init(item: UITabBarItem, action: @escaping (Bool) -> Void, contextAction: ((ContextGesture, ContextExtractedContentContainingView) -> Void)?) {
self.item = item
self.action = action
self.contextAction = contextAction
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs === rhs {
return true
}
if lhs.item !== rhs.item {
return false
}
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
return false
}
return true
}
}
public let theme: PresentationTheme
public let items: [Item]
public let selectedId: AnyHashable?
public init(
theme: PresentationTheme,
items: [Item],
selectedId: AnyHashable?
) {
self.theme = theme
self.items = items
self.selectedId = selectedId
}
public static func ==(lhs: TabBarComponent, rhs: TabBarComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.selectedId != rhs.selectedId {
return false
}
return true
}
public final class View: UIView, UITabBarDelegate, UIGestureRecognizerDelegate {
private let backgroundView: GlassBackgroundView
private let selectionView: GlassBackgroundView.ContentImageView
private let contextGestureContainerView: ContextControllerSourceView
private let nativeTabBar: UITabBar?
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
private var selectedItemViews: [AnyHashable: ComponentView<Empty>] = [:]
private var itemWithActiveContextGesture: AnyHashable?
private var component: TabBarComponent?
private weak var state: EmptyComponentState?
public override init(frame: CGRect) {
self.backgroundView = GlassBackgroundView(frame: CGRect())
self.selectionView = GlassBackgroundView.ContentImageView()
self.contextGestureContainerView = ContextControllerSourceView()
self.contextGestureContainerView.isGestureEnabled = true
if #available(iOS 26.0, *) {
self.nativeTabBar = UITabBar()
} else {
self.nativeTabBar = nil
}
super.init(frame: frame)
self.addSubview(self.contextGestureContainerView)
if let nativeTabBar = self.nativeTabBar {
self.contextGestureContainerView.addSubview(nativeTabBar)
nativeTabBar.delegate = self
/*let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.onLongPressGesture(_:)))
longPressGesture.delegate = self
self.addGestureRecognizer(longPressGesture)*/
} else {
self.contextGestureContainerView.addSubview(self.backgroundView)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
}
self.contextGestureContainerView.shouldBegin = { [weak self] point in
guard let self, let component = self.component else {
return false
}
for (id, itemView) in self.itemViews {
if let itemView = itemView.view {
if self.convert(itemView.bounds, from: itemView).contains(point) {
guard let item = component.items.first(where: { $0.id == id }) else {
return false
}
if item.contextAction == nil {
return false
}
self.itemWithActiveContextGesture = id
let startPoint = point
self.contextGestureContainerView.contextGesture?.externalUpdated = { [weak self] _, point in
guard let self else {
return
}
let dist = sqrt(pow(startPoint.x - point.x, 2.0) + pow(startPoint.y - point.y, 2.0))
if dist > 10.0 {
self.contextGestureContainerView.contextGesture?.cancel()
}
}
return true
}
}
}
return false
}
self.contextGestureContainerView.customActivationProgress = { [weak self] _, _ in
let _ = self
return
/*guard let self, let itemWithActiveContextGesture = self.itemWithActiveContextGesture else {
return
}
guard let itemView = self.itemViews[itemWithActiveContextGesture]?.view else {
return
}
let scaleSide = itemView.bounds.width
let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide)
let currentScale = 1.0 * (1.0 - progress) + minScale * progress
switch update {
case .update:
let sublayerTransform = CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0)
itemView.layer.sublayerTransform = sublayerTransform
case .begin:
let sublayerTransform = CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0)
itemView.layer.sublayerTransform = sublayerTransform
case .ended:
let sublayerTransform = CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0)
let previousTransform = itemView.layer.sublayerTransform
itemView.layer.sublayerTransform = sublayerTransform
itemView.layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2)
}*/
}
self.contextGestureContainerView.activated = { [weak self] gesture, _ in
guard let self, let component = self.component else {
return
}
guard let itemWithActiveContextGesture = self.itemWithActiveContextGesture else {
return
}
var itemView: ItemComponent.View?
if self.nativeTabBar != nil {
itemView = self.selectedItemViews[itemWithActiveContextGesture]?.view as? ItemComponent.View
} else {
itemView = self.itemViews[itemWithActiveContextGesture]?.view as? ItemComponent.View
}
guard let itemView else {
return
}
DispatchQueue.main.async { [weak self] in
guard let self else {
return
}
if let nativeTabBar = self.nativeTabBar {
func cancelGestures(view: UIView) {
for recognizer in view.gestureRecognizers ?? [] {
if NSStringFromClass(type(of: recognizer)).contains("sSelectionGestureRecognizer") {
recognizer.state = .cancelled
}
}
for subview in view.subviews {
cancelGestures(view: subview)
}
}
cancelGestures(view: nativeTabBar)
}
}
guard let item = component.items.first(where: { $0.id == itemWithActiveContextGesture }) else {
return
}
item.contextAction?(gesture, itemView.contextContainerView)
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
guard let component = self.component else {
return
}
if let index = tabBar.items?.firstIndex(where: { $0 === item }) {
if index < component.items.count {
component.items[index].action(false)
}
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
@objc private func onLongPressGesture(_ recognizer: UILongPressGestureRecognizer) {
if case .began = recognizer.state {
if let nativeTabBar = self.nativeTabBar {
func cancelGestures(view: UIView) {
for recognizer in view.gestureRecognizers ?? [] {
if NSStringFromClass(type(of: recognizer)).contains("sSelectionGestureRecognizer") {
recognizer.state = .cancelled
}
}
for subview in view.subviews {
cancelGestures(view: subview)
}
}
cancelGestures(view: nativeTabBar)
}
}
}
@objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
guard let component = self.component else {
return
}
if case .ended = recognizer.state {
let point = recognizer.location(in: self)
var closestItemView: (AnyHashable, CGFloat)?
for (id, itemView) in self.itemViews {
guard let itemView = itemView.view else {
continue
}
let distance = abs(point.x - itemView.center.x)
if let previousClosestItemView = closestItemView {
if previousClosestItemView.1 > distance {
closestItemView = (id, distance)
}
} else {
closestItemView = (id, distance)
}
}
if let (id, _) = closestItemView {
guard let item = component.items.first(where: { $0.id == id }) else {
return
}
item.action(false)
/*if previousSelectedIndex != closestNode.0 {
if let selectedIndex = self.selectedIndex, let _ = self.tabBarItems[selectedIndex].item.animationName {
container.imageNode.animationNode.play(firstFrame: false, fromIndex: nil)
}
}*/
}
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
public func frameForItem(at index: Int) -> CGRect? {
guard let component = self.component else {
return nil
}
if index < 0 || index >= component.items.count {
return nil
}
guard let itemView = self.itemViews[component.items[index].id]?.view else {
return nil
}
return self.convert(itemView.bounds, from: itemView)
}
func update(component: TabBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let innerInset: CGFloat = 3.0
let availableSize = CGSize(width: min(500.0, availableSize.width), height: availableSize.height)
let previousComponent = self.component
self.component = component
self.state = state
self.overrideUserInterfaceStyle = component.theme.overallDarkAppearance ? .dark : .light
if let nativeTabBar = self.nativeTabBar {
if nativeTabBar.items?.count != component.items.count {
nativeTabBar.items = (0 ..< component.items.count).map { i in
return UITabBarItem(title: " ", image: nil, tag: i)
}
for (_, itemView) in self.itemViews {
itemView.view?.removeFromSuperview()
}
for (_, selectedItemView) in self.selectedItemViews {
selectedItemView.view?.removeFromSuperview()
}
if let index = component.items.firstIndex(where: { $0.id == component.selectedId }) {
nativeTabBar.selectedItem = nativeTabBar.items?[index]
}
}
let nativeSize = nativeTabBar.sizeThatFits(availableSize)
nativeTabBar.bounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: nativeSize.height))
nativeTabBar.layoutSubviews()
}
var nativeItemContainers: [Int: UIView] = [:]
var nativeSelectedItemContainers: [Int: UIView] = [:]
if let nativeTabBar = self.nativeTabBar {
for subview in nativeTabBar.subviews {
if NSStringFromClass(type(of: subview)).contains("PlatterView") {
for subview in subview.subviews {
if NSStringFromClass(type(of: subview)).hasSuffix("SelectedContentView") {
for subview in subview.subviews {
if NSStringFromClass(type(of: subview)).hasSuffix("TabButton") {
nativeSelectedItemContainers[nativeSelectedItemContainers.count] = subview
}
}
} else if NSStringFromClass(type(of: subview)).hasSuffix("ContentView") {
for subview in subview.subviews {
if NSStringFromClass(type(of: subview)).hasSuffix("TabButton") {
nativeItemContainers[nativeItemContainers.count] = subview
}
}
}
}
}
}
}
var itemSize = CGSize(width: floor((availableSize.width - innerInset * 2.0) / CGFloat(component.items.count)), height: 56.0)
itemSize.width = min(94.0, itemSize.width)
if let itemContainer = nativeItemContainers[0] {
itemSize = itemContainer.bounds.size
}
let contentHeight = itemSize.height + innerInset * 2.0
var contentWidth: CGFloat = innerInset
if self.selectionView.image?.size.height != itemSize.height {
self.selectionView.image = generateStretchableFilledCircleImage(radius: itemSize.height * 0.5, color: .white)?.withRenderingMode(.alwaysTemplate)
}
self.selectionView.tintColor = component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05)
var validIds: [AnyHashable] = []
var selectionFrame: CGRect?
for index in 0 ..< component.items.count {
let item = component.items[index]
validIds.append(item.id)
let itemView: ComponentView<Empty>
var itemTransition = transition
if let current = self.itemViews[item.id] {
itemView = current
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = ComponentView()
self.itemViews[item.id] = itemView
}
let selectedItemView: ComponentView<Empty>
if let current = self.selectedItemViews[item.id] {
selectedItemView = current
} else {
selectedItemView = ComponentView()
self.selectedItemViews[item.id] = selectedItemView
}
let isItemSelected = component.selectedId == item.id
let _ = itemView.update(
transition: itemTransition,
component: AnyComponent(ItemComponent(
item: item,
theme: component.theme,
isSelected: self.nativeTabBar == nil ? isItemSelected : false
)),
environment: {},
containerSize: itemSize
)
let _ = selectedItemView.update(
transition: itemTransition,
component: AnyComponent(ItemComponent(
item: item,
theme: component.theme,
isSelected: true
)),
environment: {},
containerSize: itemSize
)
let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: floor((contentHeight - itemSize.height) * 0.5)), size: itemSize)
if let itemComponentView = itemView.view as? ItemComponent.View, let selectedItemComponentView = selectedItemView.view as? ItemComponent.View {
if itemComponentView.superview == nil {
itemComponentView.isUserInteractionEnabled = false
selectedItemComponentView.isUserInteractionEnabled = false
if self.nativeTabBar != nil {
if let itemContainer = nativeItemContainers[index] {
itemContainer.addSubview(itemComponentView)
}
if let itemContainer = nativeSelectedItemContainers[index] {
itemContainer.addSubview(selectedItemComponentView)
}
} else {
self.contextGestureContainerView.addSubview(itemComponentView)
}
}
if self.nativeTabBar != nil {
if let parentView = itemComponentView.superview {
let itemFrame = CGRect(origin: CGPoint(x: floor((parentView.bounds.width - itemSize.width) * 0.5), y: floor((parentView.bounds.height - itemSize.height) * 0.5)), size: itemSize)
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
itemTransition.setFrame(view: selectedItemComponentView, frame: itemFrame)
}
} else {
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
if let previousComponent, previousComponent.selectedId != item.id, isItemSelected {
itemComponentView.playSelectionAnimation()
selectedItemComponentView.playSelectionAnimation()
}
}
if isItemSelected {
selectionFrame = itemFrame
}
contentWidth += itemFrame.width
}
contentWidth += innerInset
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
itemView.view?.removeFromSuperview()
self.selectedItemViews[id]?.view?.removeFromSuperview()
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
self.selectedItemViews.removeValue(forKey: id)
}
if let selectionFrame, self.nativeTabBar == nil {
var selectionViewTransition = transition
if self.selectionView.superview == nil {
selectionViewTransition = selectionViewTransition.withAnimation(.none)
self.backgroundView.contentView.addSubview(self.selectionView)
}
selectionViewTransition.setFrame(view: self.selectionView, frame: selectionFrame)
} else if self.selectionView.superview != nil {
self.selectionView.removeFromSuperview()
}
let size = CGSize(width: min(availableSize.width, contentWidth), height: contentHeight)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.list.plainBackgroundColor.withMultipliedAlpha(0.75)), transition: transition)
if let nativeTabBar = self.nativeTabBar {
transition.setFrame(view: nativeTabBar, frame: CGRect(origin: CGPoint(x: floor((size.width - nativeTabBar.bounds.width) * 0.5), y: 0.0), size: nativeTabBar.bounds.size))
}
transition.setFrame(view: self.contextGestureContainerView, frame: CGRect(origin: CGPoint(), size: size))
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class ItemComponent: Component {
let item: TabBarComponent.Item
let theme: PresentationTheme
let isSelected: Bool
init(item: TabBarComponent.Item, theme: PresentationTheme, isSelected: Bool) {
self.item = item
self.theme = theme
self.isSelected = isSelected
}
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
if lhs.item != rhs.item {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
return true
}
final class View: UIView {
let contextContainerView: ContextExtractedContentContainingView
private var imageIcon: ComponentView<Empty>?
private var animationIcon: ComponentView<Empty>?
private let title = ComponentView<Empty>()
private var badge: ComponentView<Empty>?
private var component: ItemComponent?
private weak var state: EmptyComponentState?
private var setImageListener: Int?
private var setSelectedImageListener: Int?
private var setBadgeListener: Int?
override init(frame: CGRect) {
self.contextContainerView = ContextExtractedContentContainingView()
super.init(frame: frame)
self.addSubview(self.contextContainerView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
if let component = self.component {
if let setImageListener = self.setImageListener {
component.item.item.removeSetImageListener(setImageListener)
}
if let setSelectedImageListener = self.setSelectedImageListener {
component.item.item.removeSetSelectedImageListener(setSelectedImageListener)
}
if let setBadgeListener = self.setBadgeListener {
component.item.item.removeSetBadgeListener(setBadgeListener)
}
}
}
func playSelectionAnimation() {
if let animationIconView = self.animationIcon?.view as? LottieComponent.View {
animationIconView.playOnce()
}
}
func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
if previousComponent?.item.item !== component.item.item {
if let setImageListener = self.setImageListener {
self.component?.item.item.removeSetImageListener(setImageListener)
}
if let setSelectedImageListener = self.setSelectedImageListener {
self.component?.item.item.removeSetSelectedImageListener(setSelectedImageListener)
}
if let setBadgeListener = self.setBadgeListener {
self.component?.item.item.removeSetBadgeListener(setBadgeListener)
}
self.setImageListener = component.item.item.addSetImageListener { [weak self] _ in
guard let self else {
return
}
self.state?.updated(transition: .immediate, isLocal: true)
}
self.setSelectedImageListener = component.item.item.addSetSelectedImageListener { [weak self] _ in
guard let self else {
return
}
self.state?.updated(transition: .immediate, isLocal: true)
}
self.setBadgeListener = UITabBarItem_addSetBadgeListener(component.item.item) { [weak self] _ in
guard let self else {
return
}
self.state?.updated(transition: .immediate, isLocal: true)
}
}
self.component = component
self.state = state
if let animationName = component.item.item.animationName {
if let imageIcon = self.imageIcon {
self.imageIcon = nil
imageIcon.view?.removeFromSuperview()
}
let animationIcon: ComponentView<Empty>
var iconTransition = transition
if let current = self.animationIcon {
animationIcon = current
} else {
iconTransition = iconTransition.withAnimation(.none)
animationIcon = ComponentView()
self.animationIcon = animationIcon
}
let iconSize = animationIcon.update(
transition: iconTransition,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(
name: animationName
),
color: component.isSelected ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor,
placeholderColor: nil,
startingPosition: .end,
size: CGSize(width: 48.0, height: 48.0),
loop: false
)),
environment: {},
containerSize: CGSize(width: 48.0, height: 48.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: -4.0), size: iconSize).offsetBy(dx: component.item.item.animationOffset.x, dy: component.item.item.animationOffset.y)
if let animationIconView = animationIcon.view {
if animationIconView.superview == nil {
if let badgeView = self.badge?.view {
self.contextContainerView.contentView.insertSubview(animationIconView, belowSubview: badgeView)
} else {
self.contextContainerView.contentView.addSubview(animationIconView)
}
}
iconTransition.setFrame(view: animationIconView, frame: iconFrame)
}
} else {
if let animationIcon = self.animationIcon {
self.animationIcon = nil
animationIcon.view?.removeFromSuperview()
}
let imageIcon: ComponentView<Empty>
var iconTransition = transition
if let current = self.imageIcon {
imageIcon = current
} else {
iconTransition = iconTransition.withAnimation(.none)
imageIcon = ComponentView()
self.imageIcon = imageIcon
}
let iconSize = imageIcon.update(
transition: iconTransition,
component: AnyComponent(Image(
image: component.isSelected ? component.item.item.selectedImage : component.item.item.image,
tintColor: nil,
contentMode: .center
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: 3.0), size: iconSize)
if let imageIconView = imageIcon.view {
if imageIconView.superview == nil {
if let badgeView = self.badge?.view {
self.contextContainerView.contentView.insertSubview(imageIconView, belowSubview: badgeView)
} else {
self.contextContainerView.contentView.addSubview(imageIconView)
}
}
iconTransition.setFrame(view: imageIconView, frame: iconFrame)
}
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.item.item.title ?? " ", font: Font.semibold(10.0), textColor: component.isSelected ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: availableSize.height - 9.0 - titleSize.height), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.contextContainerView.contentView.addSubview(titleView)
}
titleView.frame = titleFrame
}
if let badgeText = component.item.item.badgeValue, !badgeText.isEmpty {
let badge: ComponentView<Empty>
var badgeTransition = transition
if let current = self.badge {
badge = current
} else {
badgeTransition = badgeTransition.withAnimation(.none)
badge = ComponentView()
self.badge = badge
}
let badgeSize = badge.update(
transition: badgeTransition,
component: AnyComponent(TextBadgeComponent(
text: badgeText,
font: Font.regular(13.0),
background: component.theme.rootController.tabBar.badgeBackgroundColor,
foreground: component.theme.rootController.tabBar.badgeTextColor,
insets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 1.0, right: 6.0)
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let contentWidth: CGFloat = 25.0
let badgeFrame = CGRect(origin: CGPoint(x: floor(availableSize.width / 2.0) + contentWidth - badgeSize.width - 5.0, y: -1.0), size: badgeSize)
if let badgeView = badge.view {
if badgeView.superview == nil {
self.contextContainerView.contentView.addSubview(badgeView)
}
badgeTransition.setFrame(view: badgeView, frame: badgeFrame)
}
} else if let badge = self.badge {
self.badge = nil
badge.view?.removeFromSuperview()
}
transition.setFrame(view: self.contextContainerView, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setFrame(view: self.contextContainerView.contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
self.contextContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}