Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion crates/window-core/src/authoring/art.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use serde::Deserialize;
use sha2::{Digest, Sha256};

use super::style::{FrameDto, SpriteDto};
use crate::model::{Art, SpriteDef};
use crate::ir::Rgb;
use crate::model::{Art, HoverFit, SpriteDef};
use crate::{Error, Result};

/// The prefix of every interned art name. Catalog sprite names cannot contain `/`, so interned names never clash
Expand Down Expand Up @@ -88,6 +89,87 @@ pub(super) fn intern(table: &mut Art, art: &mut Option<ArtRefDto>, usage: ArtUse
Ok(())
}

/// A region's `hover`: whether it gets the generated outline, or the art it draws instead.
#[derive(Clone, Debug)]
pub(super) enum HoverDto {
Outline(bool),
Art { art: Option<ArtRefDto>, fit: HoverFit, tint: Option<Rgb> },
}

/// The object form of `hover`.
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct HoverArtDto {
art: ArtRefDto,
fit: Option<String>,
tint: Option<serde_json::Value>,
}

impl<'de> Deserialize<'de> for HoverDto {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error as _;
let value = match serde_json::Value::deserialize(deserializer)? {
serde_json::Value::Bool(outline) => return Ok(Self::Outline(outline)),
value => value,
};
// An art value names its kind in `art`; the object form holds an art value there.
if !value.get("art").is_some_and(serde_json::Value::is_object) {
let art = serde_json::from_value(value)
.map_err(|_| D::Error::custom("`hover` must be a boolean, an art value, or `{ art, fit, tint }`"))?;
return Ok(Self::Art { art: Some(art), fit: HoverFit::Stretch, tint: None });
}
let hover: HoverArtDto =
serde_json::from_value(value).map_err(|error| D::Error::custom(format!("`hover`: {error}")))?;
let fit = match hover.fit.as_deref() {
None | Some("stretch") => HoverFit::Stretch,
Some("center") => HoverFit::Center,
Some(other) => {
return Err(D::Error::custom(format!(
"`hover` has unknown `fit` `{other}`; expected \"stretch\" or \"center\""
)));
}
};
let tint =
match hover.tint {
None => None,
Some(serde_json::Value::String(color)) => Some(Rgb::parse_hex(&color).ok_or_else(|| {
D::Error::custom(format!("`hover` has invalid `tint` `{color}`; expected #rrggbb"))
})?),
Some(_) => {
return Err(D::Error::custom(
"`hover` takes a fixed #rrggbb `tint`; a `tint(...)` handle is not supported on a hover",
));
}
};
Ok(Self::Art { art: Some(hover.art), fit, tint })
}
}

impl HoverDto {
/// Interns hover art into `table`, as a frame when it stretches and as an image when it is centered.
pub(super) fn intern(&mut self, table: &mut Art) -> Result<()> {
match self {
Self::Outline(_) => Ok(()),
Self::Art { art, fit: HoverFit::Stretch, .. } => intern(table, art, ArtUse::Frame),
Self::Art { art, fit: HoverFit::Center, .. } => {
if let Some(ArtRefDto::Inline(inline)) = art
&& inline.art == "shape"
&& !(inline.def.contains_key("width") && inline.def.contains_key("height"))
{
return Err(Error::Validation(
"`fit: \"center\"` draws the art at its own size, so its shape needs a `width` and `height`"
.into(),
));
}
intern(table, art, ArtUse::Image)
}
}
}
}

fn intern_inline(table: &mut Art, inline: &InlineArtDto, usage: ArtUse) -> Result<String> {
let def = inline.def(usage)?;
let kind = if usage == ArtUse::Frame { "frame" } else { "sprite" };
Expand Down
15 changes: 12 additions & 3 deletions crates/window-core/src/authoring/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::collections::BTreeSet;

use serde::Deserialize;

use super::art::{ArtRefDto, ArtUse, intern};
use super::art::{ArtRefDto, ArtUse, HoverDto, intern};
use super::flex::{FlexStyleDto, ItemLayoutDto};
use super::insets::InsetsDto;
use super::parse::json_object_fields;
Expand Down Expand Up @@ -46,7 +46,7 @@ pub(super) struct ElementDto {
color: Option<ColorDto>,
tint: Option<ColorDto>,
tooltip: Option<TooltipDto>,
hover: Option<bool>,
hover: Option<HoverDto>,
shadow: bool,
bold: bool,
italic: bool,
Expand Down Expand Up @@ -99,7 +99,7 @@ struct ElementShapeDto {
color: Option<ColorDto>,
tint: Option<ColorDto>,
tooltip: Option<TooltipDto>,
hover: Option<bool>,
hover: Option<HoverDto>,
#[serde(default)]
shadow: bool,
#[serde(default)]
Expand Down Expand Up @@ -204,6 +204,15 @@ pub(super) fn intern_art(children: &mut [ElementDto], table: &mut Art) -> Result
intern(table, &mut child.track, ArtUse::Frame)?;
intern(table, &mut child.art, ArtUse::Image)?;
intern(table, &mut child.selected_sprite, ArtUse::Sprite)?;
if let Some(hover) = &mut child.hover {
hover.intern(table).map_err(|error| {
let region = match &child.on_click {
Some(click) => format!("region `{}`", click.id()),
None => "region".to_string(),
};
Error::Validation(format!("{region} `hover`: {error}")).with_debug_name(child.debug_name.as_deref())
})?;
}
intern_art(&mut child.children, table)?;
}
Ok(())
Expand Down
14 changes: 11 additions & 3 deletions crates/window-core/src/authoring/element/build.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use super::{ElementDto, convert_layout_children};
use crate::authoring::art::ArtRefDto;
use crate::authoring::art::{ArtRefDto, HoverDto};
use crate::authoring::flex::parse_auto_flow;
use crate::authoring::parse::validate_name;
use crate::inventory::{InventorySlotSection, SlotRectClaim};
use crate::model::{Bar, Element, FlexBox, Region, SlotSection, Switch, SwitchCase};
use crate::model::{Bar, Element, FlexBox, Hover, HoverArt, Region, SlotSection, Switch, SwitchCase};
use crate::{Error, Result};

/// The widest and tallest a bar may be: one font atlas page, which its largest fill glyph must fit.
Expand Down Expand Up @@ -200,10 +200,18 @@ impl ElementDto {
}

fn build_region(self) -> Result<Element> {
let hover = match &self.hover {
None => None,
Some(HoverDto::Outline(false)) => Some(Hover::None),
Some(HoverDto::Outline(true)) => Some(Hover::Outline),
Some(HoverDto::Art { art, fit, tint }) => {
art_name(art)?.map(|name| Hover::Art(HoverArt { name, fit: *fit, tint: *tint }))
}
};
Ok(Element::Region(Box::new(Region {
size: self.optional_size()?,
tooltip: self.tooltip()?,
hover: self.hover,
hover,
default_action: self.default_action,
item_model: self.item_model,
name: self.name,
Expand Down
7 changes: 4 additions & 3 deletions crates/window-core/src/hud/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ pub struct ShaderOutput {
pub warnings: Vec<String>,
}

/// Color of the corner pixels of a hover outline glyph, by which the text shader finds hover glyphs.
/// Color of the corner pixels of a hover glyph, by which the text shader finds hover glyphs.
pub const HOVER_GLYPH_ID: Rgb = Rgb { r: 0x57, g: 0x48, b: 0x56 };

/// Blue channel of the pixel beside each corner of a hover outline glyph, whose red and green hold the glyph's
/// width and height minus one. A glyph is a hover glyph only with both this and [`HOVER_GLYPH_ID`].
/// Blue channel of the pixel beside each corner of a hover glyph, whose red and green hold the width and height
/// minus one of the art between the glyph's top and bottom rows. A glyph is a hover glyph only with both this and
/// [`HOVER_GLYPH_ID`].
pub const HOVER_SIZE_MARK: u8 = 0xa9;

/// Emits core text shaders that relocate shader-placed HUDs, where `row_offsets` holds the rows each of `huds` is
Expand Down
30 changes: 20 additions & 10 deletions crates/window-core/src/hud/renderer/hover.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
//! Hover outline snippets of the pack_format 85-88 `core/text` shaders.
//! Hover snippets of the pack_format 85-88 `core/text` shaders.
//!
//! A hover glyph rides in a tooltip title, so the client draws it exactly while its region is hovered. It is drawn
//! twice: the text copy, which the shader hides, and the shadow copy, which the shader moves onto the region. The
//! shadow color tells it where:
//! A hover glyph rides in a tooltip title, so the client draws it exactly while its region is hovered. The glyph is
//! the hover's art, in its own colors, between a top and a bottom row of data pixels that tell the shader the glyph
//! is a hover and how large its art is. It is drawn twice: the text copy, which the shader hides, and the shadow
//! copy, which the shader moves so its art is where the shadow color says:
//!
//! - red and green: the region's left and top edge relative to the GUI center, plus 128;
//! - red and green: the art's left and top edge relative to the GUI center, plus 128;
//! - blue and the top 3 bits of alpha: the width of the tooltip's text;
//! - the low 5 bits of alpha: the tooltip's line count plus 1, at most 30, so alpha is never 0 or 255.
//!
//! The fragment shader discards the outline inside the tooltip box those describe.
//! The fragment shader discards the data rows and the art inside the tooltip box those describe.

use crate::hud::{HOVER_GLYPH_ID, HOVER_SIZE_MARK};

Expand All @@ -23,6 +24,7 @@ uniform sampler2D Sampler0;

flat out int windowHover;
flat out vec4 windowHoverCutout;
flat out vec2 windowHoverRows;
out vec2 windowHoverPos;

const ivec3 WINDOW_HOVER_ID = ivec3({id_r}, {id_g}, {id_b});
Expand All @@ -38,6 +40,7 @@ ivec3 window_hover_pixel(ivec2 pixel, int direction, int index) {{
void window_hover_apply() {{
windowHover = 0;
windowHoverCutout = vec4(0.0);
windowHoverRows = vec2(0.0);
windowHoverPos = vec2(0.0);

vec2 texel = UV0 * vec2(textureSize(Sampler0, 0));
Expand All @@ -55,12 +58,16 @@ void window_hover_apply() {{
return;
}}

vec2 size = vec2(sizePixel.rg) + 1.0;
// The glyph is its art between two data rows, which land above and below where the art is drawn.
vec2 artSize = vec2(sizePixel.rg) + 1.0;
vec2 size = artSize + vec2(0.0, 2.0);
vec2 guiSize = ceil(window_hud_gui_size() - 0.001);
vec2 pos = floor(guiSize * 0.5) + vec2(color.rg) - 128.0 + corner * size;
vec2 art = floor(guiSize * 0.5) + vec2(color.rg) - 128.0;
vec2 pos = art + corner * size - vec2(0.0, 1.0);
gl_Position = ProjMat * ModelViewMat * vec4(pos, Position.z, 1.0);
vertexColor = vec4(1.0);
windowHover = 1;
windowHoverRows = vec2(art.y, art.y + artSize.y);
windowHoverPos = pos;

int lines = (color.a & 31) - 1;
Expand All @@ -84,16 +91,19 @@ pub(super) const HOVER_FRAGMENT_INPUTS: &str = "
#ifdef IS_GUI
flat in int windowHover;
flat in vec4 windowHoverCutout;
flat in vec2 windowHoverRows;
in vec2 windowHoverPos;
#endif";

/// Paints a hover glyph's pixels white, whatever data their colors hold, except inside its tooltip's box.
/// Discards a hover glyph's data rows, and its art inside its tooltip's box.
pub(super) const HOVER_FRAGMENT_DRAW: &str = "
#ifdef IS_GUI
if (windowHover == 1) {
if (windowHoverPos.y < windowHoverRows.x || windowHoverPos.y > windowHoverRows.y) {
discard;
}
if (all(greaterThan(windowHoverPos, windowHoverCutout.xy)) && all(lessThan(windowHoverPos, windowHoverCutout.zw))) {
discard;
}
texColor.rgb = vec3(1.0);
}
#endif";
1 change: 1 addition & 0 deletions crates/window-core/src/hud/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ fn hover_outlines_join_the_define_variant_shaders_with_or_without_huds() {
assert!(both.files[0].contents.contains("#endif\n#ifdef IS_GUI\n window_hover_apply();\n#endif\n}"));
assert!(both.files[1].contents.contains("if (windowHudTextureMode != 1) {"));
assert!(both.files[1].contents.contains("if (windowHover == 1) {"));
assert!(!both.files[1].contents.contains("texColor.rgb ="), "a hover glyph keeps its texture's colors");

// Without a shader-placed HUD there are no rule tables and no text background to relocate.
let alone = emit(Some(88), &[], &[], true).unwrap();
Expand Down
19 changes: 17 additions & 2 deletions crates/window-core/src/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,21 @@ pub struct SpriteSlotIr {
pub source: Option<String>,
}

/// What a region draws over itself while one of its slots is hovered.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HoverIr {
None,
/// The generated 1px white outline of the region's rect.
Outline,
/// Authored art.
Art {
/// The art's interned name under `frame/` or `image/`, the table it is interned in.
name: String,
/// The art at its place in GUI space, which may extend past the region; [`Draw::Tinted`] when it is tinted.
draw: Draw,
},
}

/// An inventory region: slots it claims and fills with its hitbox item, and the action its clicks name.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RegionIr {
Expand All @@ -428,8 +443,8 @@ pub struct RegionIr {
pub default_action: Option<String>,
/// The hitbox item filling the region's slots; `None` leaves them empty.
pub hitbox: Option<Hitbox>,
/// The authored `hover`: whether the region is outlined while hovered. `None` outlines a region with an action.
pub hover: Option<bool>,
/// The authored `hover`: what the region draws while hovered. `None` outlines a region with an action.
pub hover: Option<HoverIr>,
/// The authored element this region comes from, such as ``region `buy` ``.
pub source: String,
}
Expand Down
3 changes: 2 additions & 1 deletion crates/window-core/src/layout/controls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ impl<T: LayoutTarget> Solver<'_, T> {
None => format!("region `{name}`"),
};
self.check_inside_bounds(rect, &source)?;
let hover = region.hover.as_ref().map(|hover| self.hover_ir(hover, rect, &source)).transpose()?;
let hitbox = (region.tooltip.is_some() || region.item_model.is_some())
.then(|| Hitbox { item_model: region.item_model.clone(), tooltip: region.tooltip.clone() });
self.regions.push(RegionIr {
Expand All @@ -79,7 +80,7 @@ impl<T: LayoutTarget> Solver<'_, T> {
unowned: false,
default_action: region.default_action.clone(),
hitbox,
hover: region.hover,
hover,
source,
});
Ok(())
Expand Down
Loading
Loading