Saltar al contenido
zabloo

Slider

Un número que el jugador fija arrastrando. El fill y el tirador se colocan a partir del propio valor — la única disposición que el paso de layout normal no sabe producir.

primitivedesde v1focusable

Un Slider es un track que el jugador arrastra para fijar un número. Usas uno cuando un valor es continuo y aproximado —un volumen, la sensibilidad del ratón, un brillo— y notarlo moverse importa más que la cifra exacta. Piensa en un panel de ajustes de audio: la fila Master volume es un slider que escribe en un número del que es dueño el juego, con un Text enlazado al mismo path enseñándolo. Es el espejo de una ProgressBar — la misma geometría, pero un slider lo fija el jugador y una barra la fija el juego.

slider-range.viewIR v1
Two sliders in a panel: Master volume, filled purple to about two thirds with 0.65 read out beside its label, and Brightness, filled gold to about a third with 40 beside it.
ESTADO
  • hover — inactivo
  • pressed — inactivo
  • focused — inactivo
  • selected — inactivo
  • disabled — inactivo

Se lee del último frame dibujado

Parado: pulsa Ejecutar para dibujarlo en la GPU.

import { Column, Row, Slider, Text } from "@zabloo/react";


const TRACK = { background: "{color.slot}", radius: "{radius.pill}" } as const;
const FILL = { background: "{color.brand}", radius: "{radius.pill}" } as const;
const THUMB = { background: "{color.text}", radius: "{radius.pill}" } as const;

export default function SliderRange() {
  return (
    <Column
      layout={{ grow: 1, justify: "center", align: "center", padding: "{space.6}" }}
      style={{ background: "{color.bg}" }}
    >
      <Column
        id="panel"
        layout={{ width: 420, padding: "{space.5}", gap: "{space.5}", align: "stretch" }}
        style={{
          background: "{color.surface}",
          radius: "{radius.lg}",
          borderWidth: "{border.hairline}",
          borderColor: "{color.line}",
        }}
      >
        <Column layout={{ gap: "{space.2}", align: "stretch" }}>
          <Row layout={{ justify: "space-between", align: "center" }}>
            <Text style={{ color: "{color.muted}", fontSize: "{text.sm}" }}>Master volume</Text>
            {/* Same path as the slider. The label follows the drag because both
                read the game's data, not each other. */}
            <Text bind="settings.volume" style={{ color: "{color.text}", fontSize: "{text.sm}" }} />
          </Row>
          {/* Continuous: the two hooks split the two questions a game asks about
              a drag — preview it live, apply it once the player lets go. */}
          <Slider
            id="volume"
            value={{ bind: "settings.volume" }}
            onChange="volume-preview"
            onCommit="volume-apply"
            length={380}
            style={TRACK}
            fill={FILL}
            thumb={THUMB}
          />
        </Column>

        <Column layout={{ gap: "{space.2}", align: "stretch" }}>
          <Row layout={{ justify: "space-between", align: "center" }}>
            <Text style={{ color: "{color.muted}", fontSize: "{text.sm}" }}>Brightness</Text>
            <Text
              bind="settings.brightness"
              style={{ color: "{color.text}", fontSize: "{text.sm}" }}
            />
          </Row>
          {/* Quantized to min + k · step. `max` is always a valid stop, even when
              the range is not a whole number of steps. */}
          <Slider
            id="brightness"
            value={{ bind: "settings.brightness" }}
            min={0}
            max={100}
            step={10}
            onCommit="brightness-apply"
            length={380}
            style={TRACK}
            fill={{ background: "{color.gold}", radius: "{radius.pill}" }}
            thumb={THUMB}
          />
        </Column>
      </Column>
    </Column>
  );
}
Dos tracks y los números que tienen al lado. Pulsa Run y arrastra uno, y mira la etiqueta: se mueve porque está enlazada al mismo path en el que escribe el slider, no porque nadie haya cableado los dos nodos entre sí. El valor sale hacia el almacén de datos del juego y vuelve.

Las dos tablas de abajo se diferencian de una manera que vale la pena nombrar, porque es lo que la capa de componentes está haciendo por ti: length, thickness y thumbSize son números cómodos de escribir y ya no existen cuando se publica nada — se resuelven en el layout del nodo y en los tamaños de los dos slots. Un layout explícito les sigue ganando.

Props de autoría

<Slider> emite este nodo con los dos slots ya construidos. No hay una exportación en crudo que te los deje a ti: las posiciones son una convención, y el componente es el único sitio donde está escrita.

PropTipoPor defectoDescripción
valueBindable<number>minValor actual, o un binding de lectura y escritura.
min / maxnumber0 / 1Los extremos del rango — el intervalo unidad donde vive un volumen o una proporción.
stepnumberabsentPaso de cuantización. Ausente = continuo.
axis"horizontal" | "vertical""horizontal"Orientación del track. El vertical va de abajo arriba, como un fader.
onChange / onCommitstringabsentEl hook en vivo y el de cuando se asienta.
length / thickness / thumbSizenumber200 / 6 / 18Largo y grosor del track, y tamaño del tirador, en px.
fill / thumbStyleabsentLa parte llena del track, y el tirador que va sobre él.

Además de estas, todo componente acepta las props base del nodo —id, visible, disabled, layout, style, states, transition, autofocus, clip— más variant, que el theme resuelve al exportar y que nunca aparece en la IR.

Props de IR

Lo que llega al juego, cuando ya no queda capa de autoría.

PropTipoPor defectoDescripción
valueBindable<number>minValor actual, o un binding de lectura y escritura.
min / maxnumber0 / 1Los extremos del rango.
stepnumberabsentPaso de cuantización desde min. Ausente o <= 0 = continuo.
axis"horizontal" | "vertical""horizontal"Orientación del track.
onChangestringabsentNamed action que se dispara en cada cambio de valor.
onCommitstringabsentNamed action que se dispara cuando termina un gesto.
childrenZNode[][]Dos slots posicionales: primero el fill, después el thumb.

El nodo es el track

Su style pinta el raíl con el pintado implícito de siempre —ningún comando de dibujo nuevo, ningún tercer slot— y acepta exactamente dos hijos posicionales que el SDK dispone a partir del valor en vez de colocarlos en el flujo:

SlotQué esCómo lo coloca el SDK
children[0]el filldesde el principio del track hasta la fracción del valor
children[1]el thumbsu propio tamaño, centrado en la posición del valor

El recorrido del thumb va metido hacia dentro la mitad de su propio tamaño, así que nunca pinta fuera del rect del nodo: el invariante de la border-box se cumple y el hit-testing sobre los rects del layout sigue siendo honesto. Y un Slider se mide como una hoja: los slots no aportan nada a su tamaño, y por eso un thumb de 18 px no puede definir un track de 200 px.

El valor

value es o un número inicial literal o un binding de lectura y escritura: el SDK escribe cada valor nuevo en su almacén de datos y avisa al juego. Se acota a [min, max] y, si hay step, se cuantiza a min + k · step.

max siempre es una parada válida aunque el rango no sea un número entero de pasos: 0..1 de 0.3 en 0.3 para en 0.9 y luego en 1. El jugador ve el final del track, así que dejarlo inalcanzable se leería como un control atascado; el precio es un último paso corto, que es la sorpresa más pequeña de las dos. Un rango inservible (max <= min, NaN, un step negativo) colapsa a un slider fijo en vez de a un error.

Dos hooks, porque un arrastre hace dos preguntas

onChange se dispara en cada cambio, venga de donde venga: es el hook en vivo al que se engancha una previsualización de volumen. onCommit se dispara cuando el gesto termina (se suelta el puntero, se levanta la tecla): el valor en el que el jugador se ha quedado, donde cuelga el ajuste caro de aplicar en vez de que el juego haga debounce por su cuenta. Un value enlazado se escribe en cada cambio, se declare el hook que se declare.

Comportamiento

Estados

hover, pressed y focused, más disabled — propio o heredado. pressed es el arrastre: dura lo que el puntero mantenga el control, y las flechas que mueven el valor de paso en paso no lo levantan nunca, porque no hay nada que activar. Un gesto en marcha cuando el juego deshabilita el control se cancela, no se hace commit: el valor nunca llegó a asentarse.

Focusable

Sí, salvo que esté disabled. Se queda las flechas de su propio eje y no las devuelve; las del eje contrario siguen navegando. Un slider continuo toma prestado un paso del 5% de su rango para el teclado, así que las flechas funcionan sin obligarte a declarar un step que por lo demás no quieres.

Movimiento

Con una transition, el SDK desliza hasta un valor que ha empujado el juego y salta al que el jugador está arrastrando. Un control no puede ir nunca por detrás del dedo.

Actions

El jugador arrastra → el SDK acota y cuantiza el número, lo escribe en los datos y mueve el fill y el thumb → se dispara onChange. Cuando el jugador suelta se dispara además onCommit: dos named actions, un solo gesto. En el otro sentido, SetValue(id, value) recorre esa secuencia entera en una sola llamada.

Degradación

En un SDK más antiguo

En un SDK más antiguo se ven el raíl, el fill y el tirador, pero el tirador se queda donde lo haya puesto el layout y arrastrarlo no hace nada.

Como Container: el raíl, con un fill sin tamaño y un thumb colocado en el flujo a su lado. Inerte. Los dos slots sobreviven porque son nodos normales — lo que se pierde es la disposición que dictaba el valor, que es justo lo único que este tipo existe para calcular.

Cómo oye esto el juego

El binding lleva el número; la action solo dice cuándo se ha movido. Ninguna action de la v1 lleva un valor, así que la cifra en sí llega por el canal de datos, a través del binding de lectura y escritura, que es la pata que existe exactamente para eso. Lo que añaden las dos actions es el momento, y separarlas es lo importante: previsualizar un volumen en cada frame de un arrastre es barato, y volver a aplicar un preset de gráficos no lo es.

using UnityEngine;
using Zabloo;

[RequireComponent(typeof(ZablooDocument))]
public sealed class AudioOptions : MonoBehaviour
{
  ZablooDocument _doc;

  void Start()
  {
      _doc = GetComponent<ZablooDocument>();
      _doc.OnAction += OnZablooAction;

      // The slider reads the game's own value through its binding.
      _doc.SetData("settings.volume", AudioListener.volume);
  }

  void OnDestroy()
  {
      if (_doc != null) _doc.OnAction -= OnZablooAction;
  }

  void OnZablooAction(string action)
  {
      // Follow the drag with something cheap...
      if (action == "volume-preview") PlayTickSound();

      // ...and do the expensive part once, when the player lets go.
      if (action == "volume-apply") SaveAudioSettings();
  }
}

Composición

El componente construye el fill y el thumb; tú los estilizas. Todo lo demás es el layout normal que le darías a cualquier nodo.

  • Continuo, con los dos hooks — para un ajuste que puedas previsualizar mientras se arrastra y quieras aplicar una sola vez, al soltar.
  • Cuantizado y vertical — usa step y axis para el fader de un mezclador, donde las muescas son parte del control.
  • Estilizado — el nodo es el raíl, así que su propio style pinta la ranura; usa fill y thumb para las dos piezas que mueve el valor.
  • Ninguna de las anteriores — un número que el jugador solo lee es una ProgressBar, no un Slider con el input apagado.
// Continuous, both hooks, bound to the game's data.
<Slider value={{ bind: "settings.volume" }} onChange="volume-preview" onCommit="volume-apply" />

// Quantized and vertical: a fader, running bottom-to-top.
<Slider min={0} max={100} step={10} axis="vertical" length={120} />

// Styled: the node is the rail, so its own style paints the groove.
<Slider
value={{ bind: "settings.volume" }}
style={{ background: "{color.slot}", radius: "{radius.pill}" }}
fill={{ background: "{color.brand}", radius: "{radius.pill}" }}
thumb={{ background: "{color.text}", radius: "{radius.pill}" }}
/>

// A read-only number is not a Slider. A fraction of the parent is a ProgressBar.
<ProgressBar value={{ bind: "player.hp" }} />

Relacionado