Tab.kt

/*
 * Copyright 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.tv.material

import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics

/**
 * Material Design tab.
 *
 * A default Tab, also known as a Primary Navigation Tab. Tabs organize content across different
 * screens, data sets, and other interactions.
 *
 * This should typically be used inside of a [TabRow], see the corresponding documentation for
 * example usage.
 *
 * @param selected whether this tab is selected or not
 * @param onFocus called when this tab is focused
 * @param modifier the [Modifier] to be applied to this tab
 * @param onClick called when this tab is clicked (with D-Pad Center)
 * @param enabled controls the enabled state of this tab. When `false`, this component will not
 * respond to user input, and it will appear visually disabled and disabled to accessibility
 * services.
 * @param colors these will be used by the tab when in different states (focused,
 * selected, etc.)
 * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
 * for this tab. You can create and pass in your own `remember`ed instance to observe [Interaction]s
 * and customize the appearance / behavior of this tab in different states.
 * @param content content of the [Tab]
 */
@Composable
fun Tab(
  selected: Boolean,
  onFocus: () -> Unit,
  modifier: Modifier = Modifier,
  onClick: () -> Unit = { },
  enabled: Boolean = true,
  colors: TabColors = TabDefaults.pillIndicatorTabColors(),
  interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
  content: @Composable RowScope.() -> Unit
) {
  val contentColor by
    animateColorAsState(
      getTabContentColor(
        colors = colors,
        anyTabFocused = LocalTabRowHasFocus.current,
        selected = selected,
        enabled = enabled,
      )
    )
  CompositionLocalProvider(LocalContentColor provides contentColor) {
    Row(
      modifier =
        modifier
          .semantics {
            this.selected = selected
            this.role = Role.Tab
          }
          .onFocusChanged {
            if (it.isFocused) {
              onFocus()
            }
          }
          .focusable(enabled = enabled, interactionSource)
          .clickable(
            enabled = enabled,
            role = Role.Tab,
            onClick = onClick,
          ),
      horizontalArrangement = Arrangement.Center,
      verticalAlignment = Alignment.CenterVertically,
      content = content
    )
  }
}

/**
 * Represents the colors used in a tab in different states.
 *
 * - See [TabDefaults.pillIndicatorTabColors] for the default colors used in a [Tab] when using a
 * Pill indicator.
 * - See [TabDefaults.underlinedIndicatorTabColors] for the default colors used in a [Tab] when
 * using an Underlined indicator
 */
class TabColors
internal constructor(
  private val activeContentColor: Color,
  private val selectedContentColor: Color,
  private val focusedContentColor: Color,
  private val disabledActiveContentColor: Color,
  private val disabledSelectedContentColor: Color,
) {
  /**
   * Represents the content color for this tab, depending on whether it is inactive and [enabled]
   *
   * [Tab] is inactive when the [TabRow] is not focused
   *
   * @param enabled whether the button is enabled
   */
  internal fun inactiveContentColor(enabled: Boolean): Color {
    return if (enabled) activeContentColor.copy(alpha = 0.4f)
    else disabledActiveContentColor.copy(alpha = 0.4f)
  }

  /**
   * Represents the content color for this tab, depending on whether it is active and [enabled]
   *
   * [Tab] is active when some other [Tab] is focused
   *
   * @param enabled whether the button is enabled
   */
  internal fun activeContentColor(enabled: Boolean): Color {
    return if (enabled) activeContentColor else disabledActiveContentColor
  }

  /**
   * Represents the content color for this tab, depending on whether it is selected and [enabled]
   *
   * [Tab] is selected when the current [Tab] is selected and not focused
   *
   * @param enabled whether the button is enabled
   */
  internal fun selectedContentColor(enabled: Boolean): Color {
    return if (enabled) selectedContentColor else disabledSelectedContentColor
  }

  /**
   * Represents the content color for this tab, depending on whether it is focused
   *
   * * [Tab] is focused when the current [Tab] is selected and focused
   */
  internal fun focusedContentColor(): Color {
    return focusedContentColor
  }

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other == null || other !is TabColors) return false

    if (activeContentColor != other.activeContentColor(true)) return false
    if (selectedContentColor != other.selectedContentColor(true)) return false
    if (focusedContentColor != other.focusedContentColor()) return false

    if (disabledActiveContentColor != other.activeContentColor(false)) return false
    if (disabledSelectedContentColor != other.selectedContentColor(false)) return false

    return true
  }

  override fun hashCode(): Int {
    var result = activeContentColor.hashCode()
    result = 31 * result + selectedContentColor.hashCode()
    result = 31 * result + focusedContentColor.hashCode()
    result = 31 * result + disabledActiveContentColor.hashCode()
    result = 31 * result + disabledSelectedContentColor.hashCode()
    return result
  }
}

object TabDefaults {
  /**
   * [Tab]'s content colors to in conjunction with underlined indicator
   */
  // TODO: get selected & focused values from theme
  @Composable
  fun underlinedIndicatorTabColors(
    activeContentColor: Color = LocalContentColor.current,
    selectedContentColor: Color = Color(0xFFC9C2E8),
    focusedContentColor: Color = Color(0xFFC9BFFF),
    disabledActiveContentColor: Color = activeContentColor,
    disabledSelectedContentColor: Color = selectedContentColor,
  ): TabColors =
    TabColors(
      activeContentColor = activeContentColor,
      selectedContentColor = selectedContentColor,
      focusedContentColor = focusedContentColor,
      disabledActiveContentColor = disabledActiveContentColor,
      disabledSelectedContentColor = disabledSelectedContentColor,
    )

  /**
   * [Tab]'s content colors to in conjunction with pill indicator
   */
  // TODO: get selected & focused values from theme
  @Composable
  fun pillIndicatorTabColors(
    activeContentColor: Color = LocalContentColor.current,
    selectedContentColor: Color = Color(0xFFE5DEFF),
    focusedContentColor: Color = Color(0xFF313033),
    disabledActiveContentColor: Color = activeContentColor,
    disabledSelectedContentColor: Color = selectedContentColor,
  ): TabColors =
    TabColors(
      activeContentColor = activeContentColor,
      selectedContentColor = selectedContentColor,
      focusedContentColor = focusedContentColor,
      disabledActiveContentColor = disabledActiveContentColor,
      disabledSelectedContentColor = disabledSelectedContentColor,
    )
}

/** Returns the [Tab]'s content color based on focused/selected state */
private fun getTabContentColor(
  colors: TabColors,
  anyTabFocused: Boolean,
  selected: Boolean,
  enabled: Boolean,
): Color =
  when {
    anyTabFocused && selected -> colors.focusedContentColor()
    selected -> colors.selectedContentColor(enabled)
    anyTabFocused -> colors.activeContentColor(enabled)
    else -> colors.inactiveContentColor(enabled)
  }