Primeros pasos creando blueprints con GNOME Workbench (resumen del stream de ayer)

Este post forma parte de la saga dedicada a la creación de una alternativa verdaderamente libre (o sea, GNU GPL) a Postman, Insomnia y Bruno. A su vez, esto es un resumen de texto de lo que hice en un stream de livecoding anterior. Así si te lo perdiste, es fácil de leer. Principalmente, lo que voy a contar aquí es cómo utilizar Blueprint y el lenguaje de diseño. Es un post que sirve como referencia y que estaré enlazando más adelante.

En el stream de ayer, después de contar la razón por la que quiero empezar a crear una aplicación de este estilo, empecé a fabricar el prototipo de una interfaz de usuario con GNOME Workbench. Esta aplicación permite diseñar una ventana y verla en tiempo real, para poder iterar más rápido y sin tener que recompilar código.

GNOME Workbench en acción
GNOME Workbench en un pantallazo tomado de Flathub.

Para fabricar mis ventanas, estaré usando el lenguaje de diseño Blueprint. Se trata de un DSL que sirve para crear interfaces gráficas de forma declarativa. Es un lenguaje altamente experimental y apenas hay documentación en internet, pero siempre se pueden buscar proyectos de GNOME que ya lo usen y aprender de su código.

En GTK+, todo orbita alrededor de un widget. Una ventana es un widget, un botón es un widget, y una etiqueta de texto también es un widget. Los widgets tienen tipo (por ejemplo, Button, Label, Entry, Window…), propiedades (por ejemplo, el texto del botón, o el título de la ventana), métodos (por ejemplo, el método que hace que una ventana se muestre, el método que hace que una ventana se ponga a pantalla completa…), y, finalmente, señales, que son como los eventos en otros lenguajes de programación (como la señal «botón pulsado», «ventana se está cerrando»…)

Con Blueprint es fácil declarar este tipo de interfaces de usuario. Por ejemplo, el siguiente código Blueprint crea una etiqueta cuyo texto es ‘hola mundo’:

using Gtk 4.0;

Label {
  label: 'hola mundo';
}

En Workbench, podemos ver el resultado al instante:

Un pantallazo de Workbench mostrando la etiqueta creada en el blueprint anterior.
Una label mostrándose a pantalla completa en la vista previa.

Ese using Gtk 4.0 es obligatorio porque en Blueprint es obligatorio importar GTK. Si usamos más bibliotecas de componentes, podríamos importarlas mediante más líneas de using. Para usar Adwaita o GtkSourceView, por ejemplo, habrá que hacer esto.

Podemos meter más propiedades para la etiqueta si las dejamos dentro de la misma, de una forma parecida a como se hace en CSS o lenguajes similares. Por ejemplo, le pongo la propiedad selectable para indicar que quiero que se pueda seleccionar con el ratón el texto de la etiqueta.

Un pantallazo de Workbench mostrando la misma etiqueta, pero ahora tiene la propiedad selectable y de hecho se ve que el texto está parcialmente seleccionado con el ratón.
La misma label pero ahora se puede seleccionar con el cursor.

Algunos tipos de widget pueden tener un hijo o varios hijos en su interior. Cuando agregamos un botón a una ventana, estamos haciendo que el GtkButton sea un hijo de la GtkWindow en la que se mete. Cuando agregamos un texto a un botón, en realidad por debajo es un GtkLabel que es hijo del GtkButton.

Por ejemplo, creo una ventana y le establezco algunas propiedades, y luego le meto como hijo una etiqueta de texto en su interior. Lo hago metiendo directamente un nodo dentro de otro.

using Gtk 4.0;

ApplicationWindow {
  default-width: 400;
  default-height: 300;
  title: 'Cuántas veces hago clic';
  
  Label {
    label: '0';
  }
}

En este caso, Workbench no puede mostrar de forma integrada la interfaz porque estoy pidiendo una ventana, pero si pulso el botón «Show preview window», se abre igualmente una ventana separada que se actualiza también en tiempo real.

Un pantallazo con el blueprint escrito anteriormente, mostrándolo como una ventana aparte donde sólamente aparece la etiqueta 0.
Una ventana independiente mostrando una etiqueta de texto.

¿Qué pasa si quiero ponerle más de un hijo a una ventana? Que algunos widget sólo dejan tener un hijo en su interior. Para poner más de un hijo en esos casos, tenemos algunos widgets especiales que dejan tener varios hijos.

Una GtkBox es el caso más directo. Empaqueta en horizontal o en vertical varios hijos, pero por fuera es un único widget. Por lo tanto, podemos ponerle una GtkBox como hijo de una GtkApplicationWindow, y luego meter a la GtkBox tantos hijos como necesitemos.

El siguiente ejemplo crea una Box, le pone orientación vertical para que los elementos se muestren de arriba a abajo, y además la hace homogénea para que todos los hijos se expandan de forma proporcional. Luego mete dos widgets descendientes: uno es el contador, y otro es el botón para incrementar.

using Gtk 4.0;

ApplicationWindow {
  default-width: 400;
  default-height: 300;
  title: 'Cuántas veces hago clic';

  Box {
    orientation: vertical;
    homogeneous: true;

    Label {
      label: '0';
    }

    Button {
      label: 'Contar';
    }
  }
}

El resultado final tiene este aspecto:

Un pantallazo con el blueprint escrito anteriormente, mostrándolo como una ventana aparte donde aparece el texto "0" y debajo un botón que dice Contar.
Ahora la ventana muestra dos componentes uno sobre otro.

El GTK moderno se personaliza mediante CSS. Es mejor que tener funciones de API tipo «set_border» o «set_color» y da más juego. Podemos establecer la clase de un elemento de interfaz mediante la extensión styles (que no lleva dos puntos ni punto y coma porque no es una propiedad), y luego podemos establecer directamente el CSS del elemento.

En el siguiente ejemplo, le pongo a la Label y al Button dos propiedades nuevas: halign: center; valign: center;. Con estas, le pido a GTK que no expanda los elementos para hacerlos tan grande como pueda, sino que simplemente los centre y deje espacio alrededor. Después, pongo la clase .contador a mi Label, que modifico con CSS para que se vea de forma destacada.

Graphics design is my passion.

Los widgets pueden tener un identificador. La ventaja de darles un identificador es que luego se pueden referenciar desde código, e incluso hacer un binding automático. Pongamos que quiero que la label tenga un texto dinámico que se tiene que generar desde código. Le puedo dar un identificador, y luego puedo desde código localizar un widget por su ID y usar programación para cambiar sus propiedades.

Para dotarle de un identificador a un widget todo lo que hago es especificar su nombre junto al tipo:

Label contador {
  label: '0';
}

Ahora el label tiene como ID contador. En código, puedo pedirle al builder que me extraiga un widget a partir de su ID. Por ejemplo, en Rust sería del siguiente modo:

let counter: gtk::Label = workbench::builder().object("contador").unwrap();

En el siguiente ejemplo, fabrico una ventana que muestra una etiqueta y un botón. Les doto de identificadores, y luego mediante código establezco el valor inicial de la etiqueta. También conecto un callback a la señal de click en el botón para que se incremente la variable y se vuelva a repintar la etiqueta.

Este sería el código del Blueprint:

using Gtk 4.0;

Box {
  orientation: vertical;
  halign: center;
  valign: center;
  spacing: 20;

  Label counter {
    styles [
      "title-1",
      "success",
    ]

    halign: center;
    valign: center;
  }

  Button inc {
    halign: center;
    valign: center;
    label: 'Contar';
  }
}

Y este el del código (Rust). A tener en cuenta que envuelvo el contador en un arc-mutex, así con el arc tengo una forma de referenciarlo desde varios hilos, y con el mutex tengo una forma segura de mutar su valor desde cada uno de los hilos.

pub fn main() {
    let counter: gtk::Label = workbench::builder().object("counter").unwrap();
    let button: gtk::Button = workbench::builder().object("inc").unwrap();

    let mut value = Arc::new(Mutex::new(0));
    update_value(&counter, &value);

    let mut button_value = Arc::clone(&value);
    button.connect_clicked(move |_| {
        increment(&button_value);
        update_value(&counter, &button_value);
    });
}

pub fn update_value(counter: &gtk::Label, value: &Arc<Mutex<u64>>) {
    let current = value.lock().unwrap();
    let new_text = format!("{}", current);
    counter.set_label(&new_text);
}

pub fn increment(value: &Arc<Mutex<u64>>) {
    let mut counter = value.lock().unwrap();
    *counter += 1;
}

Y esto sería todo por ahora. En el siguiente post, regresamos a la realidad y os hablo de cómo hice la primera compilación de código.

Un comentario en «Primeros pasos creando blueprints con GNOME Workbench (resumen del stream de ayer)»

  1. Pingback: Mi primer prototipo con gtk-rs (ahora sí) | danirod.es

Los comentarios están cerrados.