Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
Download file

root / crates / client / src / gui / mod.rs

mod layout;
mod menu;
mod progression;

use bytemuck::{Pod, Zeroable};
use common::inventory::ContainerKind;
use common::item::{Item, ItemStack};
use common::world::texture;
use glam::{Mat4, Vec3};

use crate::assets::RgbaImage;
use crate::interaction::{Interaction, Screen};
use crate::menu::contains;
use crate::player::LocalPlayer;
use crate::render::item;

pub use layout::{button_at, slot_at};

const ATLAS: i32 = 0;
pub const WIDGETS: i32 = 3;
pub const ICONS: i32 = 4;
pub const ITEMS: i32 = 5;
pub const FONT: i32 = 6;
pub const INVENTORY: i32 = 7;
pub const CRAFTING: i32 = 8;
pub const CHEST: i32 = 9;
pub const FURNACE: i32 = 10;
const DISPENSER: i32 = crate::assets::entity_texture("gui/trap.png");
const SIGN: i32 = crate::assets::entity_texture("item/sign.png");
const PUMPKIN_BLUR: i32 = crate::assets::entity_texture("misc/pumpkinblur.png");
const VIGNETTE: i32 = crate::assets::entity_texture("misc/vignette.png");
/// Texture flags of the GUI shader.
const BLUR: i32 = 1 << 16;
const DARKEN: i32 = 1 << 17;
/// Drawn with the inverting blend of the crosshair.
const INVERT: i32 = 1 << 18;

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct Vertex {
    position: [f32; 2],
    uv: [f32; 2],
    color: u32,
    texture: i32,
}

pub struct Geometry {
    pub vertices: Vec<Vertex>,
    pub size: [f32; 2],
    /// `ScaledResolution.scaledWidthD/scaledHeightD`: the unrounded size the GUI is projected with.
    pub projection: [f32; 2],
    pub portrait: Option<Portrait>,
    /// The crosshair's vertices, drawn with `GL_ONE_MINUS_DST_COLOR`, `GL_ONE_MINUS_SRC_COLOR`.
    pub crosshair: std::ops::Range<u32>,
    widths: [u8; 256],
    /// `GuiIngame.prevVignetteBrightness`.
    vignette: f32,
    translations: rustc_hash::FxHashMap<String, String>,
}

#[derive(Clone, Copy)]
pub struct Portrait {
    pub origin: [f32; 2],
    pub cursor: [f32; 2],
    pub split: u32,
}

/// What the full-screen overlays depend on besides the player.
#[derive(Clone, Copy)]
pub struct Overlay {
    /// The light at the player's eyes for the vignette, which only fancy graphics draw.
    pub brightness: Option<f32>,
    pub partial: f32,
    /// `isInsideOfMaterial(Material.water)`: the air meter shows.
    pub underwater: bool,
}

pub fn scaled_size([width, height]: [u32; 2], gui_scale: u8) -> ([f32; 2], f32) {
    let mut scale = 1;
    while (gui_scale == 0 || scale < u32::from(gui_scale))
        && width / (scale + 1) >= 320
        && height / (scale + 1) >= 240
    {
        scale += 1;
    }
    (
        [width.div_ceil(scale) as f32, height.div_ceil(scale) as f32],
        scale as f32,
    )
}

impl Geometry {
    pub fn new(font: &RgbaImage, translations: rustc_hash::FxHashMap<String, String>) -> Self {
        let widths = std::array::from_fn(|i| {
            if i == 32 {
                return 4;
            }
            (0..8)
                .rev()
                .find(|&x| {
                    (0..8).any(|y| {
                        let px = (i % 16 * 8 + x) * font.width as usize / 128;
                        let py = (i / 16 * 8 + y) * font.height as usize / 128;
                        font.pixels[(py * font.width as usize + px) * 4 + 2] != 0
                    })
                })
                .map_or(1, |x| x as u8 + 2)
        });
        Self {
            vertices: Vec::with_capacity(12000),
            size: [0.0; 2],
            projection: [0.0; 2],
            portrait: None,
            crosshair: 0..0,
            widths,
            vignette: 1.0,
            translations,
        }
    }

    fn quad(&mut self, points: [[f32; 2]; 4], uv: [f32; 4], texture: i32, color: u32) {
        let [u, v, w, h] = uv;
        let uvs = [[u, v], [u + w, v], [u + w, v + h], [u, v + h]];
        for i in [0, 1, 2, 0, 2, 3] {
            self.vertices.push(Vertex {
                position: points[i],
                uv: uvs[i],
                color,
                texture,
            });
        }
    }

    fn tinted(&mut self, rect: [f32; 4], uv: [f32; 4], texture: i32, color: u32) {
        let size = if texture == FONT { 128.0 } else { 256.0 };
        self.quad(corners(rect), uv.map(|v| v / size), texture, color);
    }

    fn sprite(&mut self, rect: [f32; 4], uv: [f32; 4], texture: i32) {
        self.tinted(rect, uv, texture, 0xffffffff);
    }

    /// A 9 by 9 sprite of `gui/icons.png`.
    fn icon(&mut self, [x, y]: [f32; 2], [u, v]: [f32; 2]) {
        self.sprite([x, y, 9.0, 9.0], [u, v, 9.0, 9.0], ICONS);
    }

    fn fill(&mut self, rect: [f32; 4], color: u32) {
        self.tinted(rect, [0.0; 4], -1, color);
    }

    /// `FontRenderer.drawString` takes whole GUI pixels; fractional origins would distort the glyphs.
    fn text(&mut self, text: &str, x: f32, y: f32, mut color: u32, shadow: bool) {
        let (x, y) = (x.floor(), y.floor());
        // `FontRenderer.drawString`: nearly transparent text is drawn opaque
        if color & 0xfc000000 == 0 {
            color |= 0xff000000;
        }
        if shadow {
            self.text_run(text, x + 1.0, y + 1.0, color, true);
        }
        self.text_run(text, x, y, color, false);
    }

    fn text_run(&mut self, text: &str, x: f32, y: f32, color: u32, shadow: bool) {
        for (ch, offset, color) in self.layout(text, color).0 {
            let color = if shadow {
                (color & 0xff000000) | ((color & 0xfcfcfc) >> 2)
            } else {
                color
            };
            self.tinted(
                [x + offset, y, 7.99, 7.99],
                cell(ch, 8.0, 7.99),
                FONT,
                color,
            );
        }
    }

    fn text_width(&self, text: &str) -> f32 {
        let mut width = 0.0;
        let mut chars = text.chars();
        while let Some(ch) = chars.next() {
            if ch == '§' {
                chars.next();
            } else {
                width += f32::from(self.widths[usize::from(glyph(ch))]);
            }
        }
        width
    }

    /// The full-screen effects under the HUD: the vignette in fancy graphics, the view through a
    /// pumpkin, and the swirl of a portal.
    fn overlays(&mut self, interaction: &Interaction, player: &LocalPlayer, overlay: Overlay) {
        let [w, h] = self.size;
        let screen = [[0.0, 0.0], [w, 0.0], [w, h], [0.0, h]];
        let whole = [0.0, 0.0, 1.0, 1.0];
        if let Some(brightness) = overlay.brightness {
            let darkness = (1.0 - brightness).clamp(0.0, 1.0);
            self.vignette =
                (f64::from(self.vignette) + f64::from(darkness - self.vignette) * 0.01) as f32;
            self.quad(screen, whole, VIGNETTE | BLUR | DARKEN, gray(self.vignette));
        }
        let helmet = interaction.inventory.armor[3].map(|stack| stack.item);
        if !player.third_person && helmet == Some(Item::from_block(common::world::Block::PUMPKIN)) {
            self.quad(screen, whole, PUMPKIN_BLUR | BLUR, 0xffffffff);
        }
        let portal = &player.entity.portal;
        let mut time = portal.prev_time + (portal.time - portal.prev_time) * overlay.partial;
        if time > 0.0 {
            if time < 1.0 {
                time *= time;
                time *= time;
                time = time * 0.8 + 0.2;
            }
            let tile = texture::inventory_face(common::world::Block::PORTAL, 0, 0);
            let alpha = ((time * 255.0) as u32) << 24;
            self.quad(
                screen,
                cell(tile, 1.0 / 16.0, 1.0 / 16.0),
                ATLAS,
                alpha | 0xffffff,
            );
        }
    }

    /// The F3 screen: renderer and world counters, then the player's position and facing.
    pub fn debug(&mut self, info: &[String], position: &[String]) {
        for (i, line) in info.iter().enumerate() {
            self.text(line, 2.0, 2.0 + i as f32 * 10.0, 0xffffffff, true);
        }
        for (i, line) in position.iter().enumerate() {
            self.text(line, 2.0, 64.0 + i as f32 * 8.0, 0xffe0e0e0, true);
        }
    }

    /// The record message above the hotbar, cycling through colors as it fades.
    pub fn record(&mut self, message: &str, ticks: f32) {
        let alpha = ((ticks * 256.0 / 20.0) as i32).min(255);
        if alpha <= 0 {
            return;
        }
        let [w, h] = self.size;
        let [r, g, b] = hsb_to_rgb(ticks / 50.0, 0.7, 0.6);
        let color = (alpha as u32) << 24 | r | g << 8 | b << 16;
        let x = self.centered_x(message, w / 2.0);
        self.text(message, x, (h - 48.0).floor() - 4.0, color, false);
    }

    /// Text laid out like `FontRenderer`, starting in `color`: each glyph with its x offset
    /// and the color its `§` codes give it, and the width of the whole line.
    pub fn layout(&self, text: &str, mut color: u32) -> (Vec<(u8, f32, u32)>, f32) {
        let mut x = 0.0;
        let mut glyphs = Vec::with_capacity(text.len());
        let mut chars = text.chars();
        while let Some(ch) = chars.next() {
            if ch == '§'
                && let Some(code) = chars.next()
            {
                color = code_color(code, color);
                continue;
            }
            let ch = glyph(ch);
            glyphs.push((ch, x, color));
            x += f32::from(self.widths[usize::from(ch)]);
        }
        (glyphs, x)
    }

    /// The atlas rectangle of a glyph in `font/default.png`, as normalized u, v, width, height.
    pub fn glyph_uv(ch: u8) -> [f32; 4] {
        cell(ch, 1.0 / 16.0, 7.99 / 128.0)
    }

    /// `GuiEditSign.drawScreen`: the sign seen from the front, its lines written on it, with
    /// arrows blinking around the line being edited.
    fn sign_editor(&mut self, editor: &crate::menu::SignEditor, blink: bool) {
        let [w, _] = self.size;
        let center = (w / 2.0).floor();
        let scale = 93.75;
        let post = editor.sign.block == common::world::Block::SIGN;
        // Height of the sign's origin below the top of the screen, in blocks
        let origin = if post { 1.3125 } else { 1.625 };
        let rect = |x0: f32, x1: f32, y0: f32, y1: f32| {
            [
                center + x0 * scale,
                (origin + y0) * scale,
                (x1 - x0) * scale,
                (y1 - y0) * scale,
            ]
        };
        let texture = |[u, v, uw, vh]: [f32; 4]| [u / 64.0, v / 32.0, uw / 64.0, vh / 32.0];
        let board = corners(rect(-0.5, 0.5, -14.0 / 24.0, -2.0 / 24.0));
        self.quad(board, texture([2.0, 2.0, 24.0, 12.0]), SIGN, 0xffffffff);
        if post {
            let stick = corners(rect(-1.0 / 24.0, 1.0 / 24.0, -2.0 / 24.0, 12.0 / 24.0));
            self.quad(stick, texture([2.0, 16.0, 2.0, 14.0]), SIGN, 0xffffffff);
        }
        let unit = scale / 90.0;
        let top = (origin - 1.0 / 3.0) * scale;
        for (i, line) in editor.lines.iter().enumerate() {
            let line = if blink && i == editor.line {
                format!("> {line} <")
            } else {
                line.clone()
            };
            let (glyphs, width) = self.layout(&line, 0xff000000);
            let left = center - (width / 2.0).floor() * unit;
            let y = top + (i as f32 * 10.0 - 20.0) * unit;
            for (ch, offset, color) in glyphs {
                let size = 7.99 * unit;
                let points = corners([left + offset * unit, y, size, size]);
                self.quad(points, Self::glyph_uv(ch), FONT, color);
            }
        }
    }

    fn centered(&mut self, text: &str, x: f32, y: f32) {
        self.text(text, self.centered_x(text, x), y, 0xffffffff, true);
    }

    /// `drawCenteredString`'s left edge, with its integer halving.
    fn centered_x(&self, text: &str, x: f32) -> f32 {
        x.floor() - (self.text_width(text) / 2.0).floor()
    }

    fn item(&mut self, stack: ItemStack, x: f32, y: f32) {
        self.item_icon(stack, x, y);
        self.item_overlay(stack, x, y);
    }

    /// `RenderItem.renderItemIntoGUI`.
    fn item_icon(&mut self, stack: ItemStack, x: f32, y: f32) {
        if let Some(block) = stack.item.block() {
            let texture = texture::inventory_face(block, stack.damage as u8, 0);
            if item::in_3d(block) {
                let transform = Mat4::from_translation(Vec3::new(x + 8.0, y + 8.0, 0.0))
                    * Mat4::from_scale(Vec3::new(10.0, 10.0, -10.0))
                    * Mat4::from_rotation_x(210_f32.to_radians())
                    * Mat4::from_rotation_y(-45_f32.to_radians());
                let mut faces: Vec<_> = item::faces(block, stack.damage as u8)
                    .into_iter()
                    .map(|face| {
                        let points = face.points.map(|p| transform.transform_point3(p));
                        (face, points)
                    })
                    .collect();
                faces.sort_by(|a, b| {
                    let depth = |p: &[Vec3; 4]| p.iter().map(|p| p.z).sum::<f32>();
                    depth(&a.1).total_cmp(&depth(&b.1))
                });
                for (face, points) in faces {
                    let a = points[1] - points[0];
                    let b = points[2] - points[0];
                    if a.x * b.y - a.y * b.x >= 0.0 {
                        continue;
                    }
                    let color = item::color(item::tint(block, stack.damage as u8), face.shade);
                    for i in [0, 2, 1, 0, 3, 2] {
                        self.vertices.push(Vertex {
                            position: [points[i].x, points[i].y],
                            uv: face.uv[i],
                            texture: 0,
                            color,
                        });
                    }
                }
            } else {
                self.sprite([x, y, 16.0, 16.0], cell(texture, 16.0, 15.99), ATLAS);
            }
        } else if stack.item.properties().is_some() {
            self.sprite(
                [x, y, 16.0, 16.0],
                cell(item::icon(stack), 16.0, 16.0),
                ITEMS,
            );
        }
    }

    /// `RenderItem.renderItemOverlayIntoGUI`: the stack size and the damage bar.
    fn item_overlay(&mut self, stack: ItemStack, x: f32, y: f32) {
        if stack.count > 1 {
            let count = stack.count.to_string();
            let left = x + 17.0 - self.text_width(&count);
            self.text(&count, left, y + 9.0, 0xffffffff, true);
        }
        if stack.is_damaged() {
            let fraction = stack.damage as f32 / stack.max_damage() as f32;
            let width = (13.0 - fraction * 13.0).round();
            let green = (255.0 - fraction * 255.0).round() as u32;
            self.fill([x + 2.0, y + 13.0, 13.0, 2.0], 0xff000000);
            let shade = 0xff000000 | ((255 - green) / 4) | (63 << 8);
            self.fill([x + 2.0, y + 13.0, 12.0, 1.0], shade);
            let color = 0xff000000 | (255 - green) | green << 8;
            self.fill([x + 2.0, y + 13.0, width, 1.0], color);
        }
    }

    pub fn build(
        &mut self,
        interaction: &Interaction,
        player: &LocalPlayer,
        extent: [u32; 2],
        gui_scale: u8,
        overlay: Overlay,
    ) {
        let scale = self.begin(extent, gui_scale);
        let [w, h] = self.size;
        self.overlays(interaction, player, overlay);
        let rect = [w / 2.0 - 91.0, h - 22.0, 182.0, 22.0];
        self.sprite(rect, [0.0, 0.0, 182.0, 22.0], WIDGETS);
        let selected = w / 2.0 - 92.0 + interaction.inventory.selected as f32 * 20.0;
        self.sprite(
            [selected, h - 23.0, 24.0, 22.0],
            [0.0, 22.0, 24.0, 22.0],
            WIDGETS,
        );
        let start = self.vertices.len() as u32;
        let rect = [w / 2.0 - 7.0, h / 2.0 - 7.0, 16.0, 16.0];
        self.sprite(rect, [0.0, 0.0, 16.0, 16.0], ICONS | INVERT);
        self.crosshair = start..self.vertices.len() as u32;
        for (i, stack) in interaction.inventory.main[..9].iter().enumerate() {
            if let Some(stack) = stack {
                let (x, y) = (w / 2.0 - 88.0 + i as f32 * 20.0, h - 19.0);
                let start = self.vertices.len();
                self.item_icon(*stack, x, y);
                // `GuiIngame.renderInventorySlot`: a picked up stack pops
                let pop = f32::from(interaction.pops[i]) - overlay.partial;
                if pop > 0.0 {
                    let stretch = 1.0 + pop / 5.0;
                    let scale = [1.0 / stretch, (stretch + 1.0) / 2.0];
                    let pivot = [x + 8.0, y + 12.0];
                    for vertex in &mut self.vertices[start..] {
                        for axis in 0..2 {
                            vertex.position[axis] =
                                pivot[axis] + (vertex.position[axis] - pivot[axis]) * scale[axis];
                        }
                    }
                }
                self.item_overlay(*stack, x, y);
            }
        }
        let health = player.entity.living.as_ref().map_or(20, |l| l.health);
        let previous_health = player.entity.living.as_ref().map_or(20, |l| l.prev_health);
        let flash = player.entity.hearts_life >= 10 && player.entity.hearts_life / 3 % 2 == 1;
        let mut random =
            common::JavaRandom::new(i64::from(player.entity.ticks_existed.wrapping_mul(312871)));
        let armor = interaction.inventory.armor_value();
        for i in 0..10 {
            let x = w / 2.0 - 91.0 + i as f32 * 8.0;
            let y = h - 32.0
                + if health <= 4 {
                    random.next_int(2) as f32
                } else {
                    0.0
                };
            self.icon([x, y], [if flash { 25.0 } else { 16.0 }, 0.0]);
            if flash && previous_health > i * 2 {
                let u = if previous_health == i * 2 + 1 {
                    79.0
                } else {
                    70.0
                };
                self.icon([x, y], [u, 0.0]);
            }
            if health > i * 2 {
                self.icon([x, y], [if health == i * 2 + 1 { 61.0 } else { 52.0 }, 0.0]);
            }
            if armor > 0 {
                let u = match armor.cmp(&(i * 2 + 1)) {
                    std::cmp::Ordering::Greater => 34.0,
                    std::cmp::Ordering::Equal => 25.0,
                    std::cmp::Ordering::Less => 16.0,
                };
                self.icon([w / 2.0 + 82.0 - i as f32 * 8.0, h - 32.0], [u, 9.0]);
            }
        }
        if overlay.underwater {
            let full = ((player.entity.air - 2) as f64 * 10.0 / 300.0).ceil() as i32;
            let bubbles = (player.entity.air as f64 * 10.0 / 300.0).ceil() as i32;
            for i in 0..bubbles {
                let u = if i < full { 16.0 } else { 25.0 };
                self.icon([w / 2.0 - 91.0 + i as f32 * 8.0, h - 41.0], [u, 18.0]);
            }
        }
        let timer = player.entity.sleep.timer;
        if timer > 0 {
            let fade = if timer > 100 {
                1.0 - (timer - 100) as f32 / 10.0
            } else {
                timer as f32 / 100.0
            };
            self.fill([0.0, 0.0, w, h], ((220.0 * fade) as u32) << 24 | 0x101010);
        }
        let cursor = interaction.cursor.map(|v| (v / scale).floor());
        match interaction.screen {
            Some(Screen::Inventory) => self.container(interaction, cursor),
            Some(Screen::Death) => {
                // `GuiGameOver.drawScreen`
                self.gradient([0.0, 0.0, w, h], 0x60000050, 0xa0303080);
                let start = self.vertices.len();
                self.centered("Game over!", (w as i32 / 2 / 2) as f32, 30.0);
                for vertex in &mut self.vertices[start..] {
                    vertex.position = vertex.position.map(|v| v * 2.0);
                }
                self.centered("Score: &e0", w / 2.0, 100.0);
                for (i, text) in ["Respawn", "Title menu"].into_iter().enumerate() {
                    let rect = layout::button_rect(self.size, Screen::Death, i);
                    let hover = contains(rect, cursor);
                    let v = if hover { 86.0 } else { 66.0 };
                    self.sprite(rect, [0.0, v, 200.0, 20.0], WIDGETS);
                    let color = if hover { 0xffa0ffff } else { 0xffe0e0e0 };
                    let x = self.centered_x(text, w / 2.0);
                    self.text(text, x, rect[1] + 6.0, color, true);
                }
            }
            None => {}
        }
    }

    fn container(&mut self, interaction: &Interaction, cursor: [f32; 2]) {
        let kind = interaction.container.kind;
        let [x, y, w, h] = layout::container_rect(self.size, kind);
        // `GuiScreen.drawDefaultBackground` in a world
        let screen = [0.0, 0.0, self.size[0], self.size[1]];
        self.gradient(screen, 0xc0101010, 0xd0101010);
        let texture = match kind {
            ContainerKind::Inventory => INVENTORY,
            ContainerKind::Workbench => CRAFTING,
            ContainerKind::Chest { .. } => CHEST,
            ContainerKind::Furnace => FURNACE,
            ContainerKind::Dispenser => DISPENSER,
        };
        if let ContainerKind::Chest { slots } = kind {
            let top = (slots / 9 * 18 + 17) as f32;
            self.sprite([x, y, w, top], [0.0, 0.0, w, top], texture);
            self.sprite([x, y + top, w, 96.0], [0.0, 126.0, w, 96.0], texture);
        } else {
            self.sprite([x, y, w, h], [0.0, 0.0, w, h], texture);
        }
        if kind == ContainerKind::Inventory {
            self.portrait = Some(Portrait {
                origin: [x + 51.0, y + 75.0],
                cursor,
                split: self.vertices.len() as u32,
            });
            self.text("Crafting", x + 86.0, y + 16.0, 0xff404040, false);
        } else {
            let (title, left) = match kind {
                ContainerKind::Dispenser => ("Dispenser", 60.0),
                ContainerKind::Furnace => (&*interaction.title, 60.0),
                ContainerKind::Workbench => (&*interaction.title, 28.0),
                _ => (&*interaction.title, 8.0),
            };
            self.text(title, x + left, y + 6.0, 0xff404040, false);
            self.text("Inventory", x + 8.0, y + h - 94.0, 0xff404040, false);
        }
        if kind == ContainerKind::Furnace {
            let [cook, burn, total] = interaction.bars;
            if burn > 0 {
                let flame = f32::from(burn) * 12.0 / f32::from(total.max(1));
                let flame = flame.floor().clamp(0.0, 12.0);
                let size = [14.0, flame + 2.0];
                let rect = [x + 56.0, y + 48.0 - flame, size[0], size[1]];
                self.sprite(rect, [176.0, 12.0 - flame, size[0], size[1]], FURNACE);
            }
            let width = (f32::from(cook) * 24.0 / 200.0).floor() + 1.0;
            let rect = [x + 79.0, y + 34.0, width, 16.0];
            self.sprite(rect, [176.0, 14.0, width, 16.0], FURNACE);
        }
        let mut hovered = None;
        for index in 0..kind.slot_count() {
            let [sx, sy] = layout::slot_position(kind, index);
            let container = &interaction.container;
            let item = container.get(&interaction.inventory, &interaction.external, index);
            if let Some(item) = item {
                self.item(item, x + sx, y + sy);
            }
            if contains([x + sx - 1.0, y + sy - 1.0, 18.0, 18.0], cursor) {
                self.fill([x + sx, y + sy, 16.0, 16.0], 0x80ffffff);
                hovered = hovered.or(Some(item));
            }
        }
        if let Some(item) = interaction.container.carried {
            self.item(item, cursor[0] - 8.0, cursor[1] - 8.0);
            return;
        }
        let name = hovered
            .flatten()
            .and_then(|stack| stack.translation_key())
            .and_then(|key| self.translations.get(&format!("{key}.name")))
            .map(|name| name.trim().to_owned());
        if let Some(name) = name.filter(|name| !name.is_empty()) {
            let [tx, ty] = [cursor[0] + 12.0, cursor[1] - 12.0];
            let width = self.text_width(&name);
            self.fill([tx - 3.0, ty - 3.0, width + 6.0, 14.0], 0xc0000000);
            self.text(&name, tx, ty, 0xffffffff, true);
        }
    }
}

fn corners([x, y, w, h]: [f32; 4]) -> [[f32; 2]; 4] {
    [[x, y], [x + w, y], [x + w, y + h], [x, y + h]]
}

/// The rectangle of cell `index` in a 16 by 16 grid of `step` sized cells.
fn cell(index: u8, step: f32, size: f32) -> [f32; 4] {
    [
        f32::from(index % 16) * step,
        f32::from(index / 16) * step,
        size,
        size,
    ]
}

fn gray(level: f32) -> u32 {
    0xff000000 | ((level * 255.0) as u32 * 0x010101)
}

fn glyph(ch: char) -> u8 {
    if ch.is_ascii() {
        return ch as u8;
    }
    const EXTENDED: &str = "⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜø£Ø×ƒáíóúñѪº¿®¬½¼¡«»";
    EXTENDED
        .chars()
        .position(|c| c == ch)
        .map_or(b'?', |index| 127 + index as u8)
}

/// `java.awt.Color.HSBtoRGB`.
fn hsb_to_rgb(hue: f32, saturation: f32, brightness: f32) -> [u32; 3] {
    let h = (hue - hue.floor()) * 6.0;
    let f = h - h.floor();
    let p = brightness * (1.0 - saturation);
    let q = brightness * (1.0 - saturation * f);
    let t = brightness * (1.0 - saturation * (1.0 - f));
    let (r, g, b) = match h as i32 {
        0 => (brightness, t, p),
        1 => (q, brightness, p),
        2 => (p, brightness, t),
        3 => (p, q, brightness),
        4 => (t, p, brightness),
        _ => (brightness, p, q),
    };
    [r, g, b].map(|c| (c * 255.0 + 0.5) as u32)
}

/// The color of a `§` formatting code, keeping the alpha of `color`.
fn code_color(code: char, color: u32) -> u32 {
    let code = code.to_digit(16).unwrap_or(15);
    let light = (code >> 3) * 85;
    let red = (code >> 2 & 1) * 170 + light + if code == 6 { 85 } else { 0 };
    let green = (code >> 1 & 1) * 170 + light;
    let blue = (code & 1) * 170 + light;
    (color & 0xff000000) | red | green << 8 | blue << 16
}