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
5 changes: 3 additions & 2 deletions crates/window-core/src/codegen/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,11 @@ pub(super) fn generate_fullscreen_entries(manifest: &Manifest, package_name: &st
let switches = any(&|f| !f.switches.is_empty());
let layers = any(&|f| !f.layers.is_empty());
let imports = [
(any(&|f| !f.slots.is_empty()), "Align"),
(any(&|f| !f.slots.is_empty() || f.controls.values().any(|control| control.tooltip.is_some())), "Align"),
(any(&|f| !f.art.is_empty()), "ArtEntry"),
(any(&|f| f.art.values().any(|art| art.bar.is_some())), "BarEntry"),
(any(&|f| !f.controls.is_empty()), "ControlEntry"),
(any(&|f| f.controls.values().any(|control| control.tooltip.is_some())), "ControlTooltipEntry"),
(true, "CursorEntry"),
(true, "FullscreenCanvasEntry"),
(true, "FullscreenEntry"),
Expand All @@ -220,7 +221,7 @@ pub(super) fn generate_fullscreen_entries(manifest: &Manifest, package_name: &st
(canvas.horse.is_some(), "HorseEntry"),
(layers, "LayerEntry"),
(layers, "LayerKind"),
(any(&|f| !f.slots.is_empty()), "SlotEntry"),
(any(&|f| !f.slots.is_empty() || f.controls.values().any(|control| control.tooltip.is_some())), "SlotEntry"),
(any_slot(&|slot| slot.lines.is_some()), "SlotLinesEntry"),
(switches, "SwitchCaseEntry"),
(switches, "SwitchEntry"),
Expand Down
26 changes: 19 additions & 7 deletions crates/window-core/src/codegen/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ use std::collections::BTreeMap;

use crate::ir::{FullscreenMode, Hitbox, Layer, Tooltip};
use crate::manifest::{
AnvilInputEntry, ArtEntry, BarEntry, CollectionEntry, ControlEntry, FontMetricsEntry, FullscreenCanvasEntry,
FullscreenSurfaceEntry, HoverEntry, HudShaderEntry, HudSurfaceEntry, ItemEntry, RegionEntry, SlotAreaEntry,
SlotEntry, SlotLinesEntry, SlotRefEntry, SpriteEntry, SpriteSlotEntry, SurfaceEntry, SwitchCaseEntry, SwitchEntry,
TextOverflow,
AnvilInputEntry, ArtEntry, BarEntry, CollectionEntry, ControlEntry, ControlTooltipEntry, FontMetricsEntry,
FullscreenCanvasEntry, FullscreenSurfaceEntry, HoverEntry, HudShaderEntry, HudSurfaceEntry, ItemEntry, RegionEntry,
SlotAreaEntry, SlotEntry, SlotLinesEntry, SlotRefEntry, SpriteEntry, SpriteSlotEntry, SurfaceEntry,
SwitchCaseEntry, SwitchEntry, TextOverflow,
};

use super::literals::{
Expand Down Expand Up @@ -46,7 +46,7 @@ pub(super) fn fullscreen_surface_entry_expr(surface: &FullscreenSurfaceEntry, le
}

pub(super) fn control_entry_expr(control: &ControlEntry, level: usize) -> String {
Call::new("ControlEntry", level)
let call = Call::new("ControlEntry", level)
.arg("index", control.index)
.arg("x", control.x)
.arg("y", control.y)
Expand All @@ -55,8 +55,20 @@ pub(super) fn control_entry_expr(control: &ControlEntry, level: usize) -> String
.arg("action", optional_string_expr(control.action.as_ref()))
.arg("defaultAction", optional_string_expr(control.default_action.as_ref()))
.arg("hover", control.hover)
.arg("outline", optional_string_expr(control.outline.as_ref()))
.arg("source", optional_string_expr(control.source.as_ref()))
.arg("outline", optional_string_expr(control.outline.as_ref()));
let call = match &control.tooltip {
Some(tooltip) => call.arg("tooltip", control_tooltip_entry_expr(tooltip, level + 1)),
None => call,
};
call.arg("source", optional_string_expr(control.source.as_ref())).finish()
}

fn control_tooltip_entry_expr(tooltip: &ControlTooltipEntry, level: usize) -> String {
let lines = tooltip.lines.iter().map(|line| slot_entry_expr(line, level + 2));
Call::new("ControlTooltipEntry", level)
.arg("panel", kt_string(&tooltip.panel))
.arg("marker", kt_string(&tooltip.marker))
.arg("lines", list_of(lines, level + 1))
.finish()
}

Expand Down
11 changes: 9 additions & 2 deletions crates/window-core/src/fullscreen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,16 @@ pub const BACKDROP_LAYER: u8 = 0;
pub const STATIC_LAYER: u8 = 1;

/// The highest layer of a window's runtime layers, which take the layers from `STATIC_LAYER + 1` in authored order.
pub const MAX_CONTENT_LAYER: u8 = 253;
pub const MAX_CONTENT_LAYER: u8 = 251;

/// The layer of the runtime's own overlays, such as server-driven hover outlines, above every window layer.
/// The layer of the hovered control's tooltip panel, above every window layer.
pub const TOOLTIP_LAYER: u8 = 252;

/// The layer of the hovered control's tooltip text, above its panel.
pub const TOOLTIP_TEXT_LAYER: u8 = 253;

/// The layer of the runtime's own overlays, such as server-driven hover outlines, above every window layer and
/// tooltip.
pub const OVERLAY_LAYER: u8 = 254;

/// The layer of the cursor, above everything else.
Expand Down
13 changes: 13 additions & 0 deletions crates/window-core/src/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -645,12 +645,25 @@ pub struct LaidOutFullscreen {
pub art: Vec<ArtIr>,
/// Runtime layers in authored tree order.
pub layers: Vec<Layer>,
/// The tooltips of [`Self::regions`], each drawn while its region is hovered.
pub tooltips: Vec<TooltipIr>,
/// Typed handles by id.
pub handles: BTreeMap<String, Handle>,
/// Non-fatal findings to surface to the user.
pub warnings: Vec<String>,
}

/// A full-screen region's tooltip, laid out on the canvas beside the region.
#[derive(Clone, Debug)]
pub struct TooltipIr {
/// The region's index in [`LaidOutFullscreen::regions`].
pub region: usize,
/// The panel drawn behind the text.
pub panel: Rect,
/// The title, then each line, as static labels over the panel.
pub lines: Vec<SlotIr>,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
9 changes: 5 additions & 4 deletions crates/window-core/src/layout/controls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,11 @@ impl<T: LayoutTarget> Solver<'_, T> {
None => format!("region `{name}`"),
};
self.check_inside_bounds(rect, &source)?;
if self.target.container_kind().is_none() && (region.tooltip.is_some() || region.item_model.is_some()) {
return Err(self
.target
.layout_err(format!("{source} sets `tooltip` or `item_model`, which need inventory slots")));
if self.target.container_kind().is_none() && region.item_model.is_some() {
return Err(self.target.layout_err(format!(
"{source} sets `item_model`, which draws the hitbox item of inventory slots, and a {} has none",
self.target.kind()
)));
}
let hitbox = (region.tooltip.is_some() || region.item_model.is_some())
.then(|| Hitbox { item_model: region.item_model.clone(), tooltip: region.tooltip.clone() });
Expand Down
7 changes: 3 additions & 4 deletions crates/window-core/src/layout/flex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,10 +395,9 @@ impl<T: LayoutTarget> Solver<'_, T> {
/// Lays a section's children out on a grid with one track per slot, then places each child on its area.
pub(super) fn place_section(&mut self, section: &SlotSection) -> Result<Size> {
let label = section_name(section);
let kind = self
.target
.container_kind()
.ok_or_else(|| self.target.layout_err(format!("{label} sections are only available in windows")))?;
let kind = self.target.container_kind().ok_or_else(|| {
self.target.layout_err(format!("{label} sections are only available in container windows"))
})?;
let dims = kind.section_size(section.section);
let bounds = kind.section_bounds(section.section);
if let Some(frame) = &section.frame {
Expand Down
10 changes: 10 additions & 0 deletions crates/window-core/src/layout/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod flex;
mod measure;
mod slots;
mod target;
mod tooltips;
mod visuals;

use std::collections::HashSet;
Expand Down Expand Up @@ -180,6 +181,14 @@ fn solve_fullscreen(
let mut solver = Solver::new(project, texture_size, fonts, target);
solver.place_children(&fullscreen.children, canvas)?;
let Solved { draws, slots, regions, switches, art, layers, warnings, .. } = solver.finish();
let tooltips = regions
.iter()
.enumerate()
.filter_map(|(index, region)| {
let tooltip = region.hitbox.as_ref()?.tooltip.as_ref()?;
Some(tooltips::tooltip(&target, index, region, tooltip))
})
.collect::<Result<_>>()?;

Ok(LaidOutFullscreen {
name: fullscreen.name.clone(),
Expand All @@ -191,6 +200,7 @@ fn solve_fullscreen(
switches,
art,
layers,
tooltips,
handles: fullscreen.handles.clone(),
warnings,
})
Expand Down
16 changes: 16 additions & 0 deletions crates/window-core/src/layout/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,20 @@ impl LayoutTarget for FullscreenTarget<'_> {
fn duplicate_namespace(&self) -> &'static str {
"slot/region/label names share one namespace"
}

fn sprite_slot_err(&self, name: &str) -> Error {
self.layout_err(format!(
"fullscreen `{}` cannot contain runtime sprite slot `{name}`: full-screen windows draw no runtime \
sprites; switch between static images with a `<Switch>` instead",
self.name
))
}

fn inventory_err(&self, name: &str) -> Error {
self.layout_err(format!(
"fullscreen `{}` cannot contain inventory element `{name}`: real items need inventory slots, which only \
container windows have; draw item art with an image instead",
self.name
))
}
}
94 changes: 94 additions & 0 deletions crates/window-core/src/layout/tooltips.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//! Full-screen tooltips: a vanilla-styled panel of plain text, laid out beside its region.

use super::target::LayoutTarget;
use crate::Result;
use crate::geometry::{Point, Rect, Size};
use crate::ir::{Align, RegionIr, Rgb, SlotIr, Tooltip, TooltipIr};
use crate::model::TextFit;
use crate::vanilla;

/// The title's color, as in a container tooltip.
const TITLE_COLOR: Rgb = Rgb::new(0xff, 0xff, 0xff);
/// The other lines' color, as in a container tooltip.
const LINE_COLOR: Rgb = Rgb::new(0xaa, 0xaa, 0xaa);
/// The distance from the panel's edge to its text: a 1px rim, a 1px border, and 2px of padding.
const INSET: u32 = 4;
/// Vanilla's tooltip line pitch, and the gap it leaves below the title.
const LINE_PITCH: u32 = 10;
const TITLE_GAP: u32 = 2;
/// The gap between the region and the panel.
const GAP: i32 = 2;

/// The tooltip of `regions[index]`, centered below its region, or above it where the canvas ends first, and kept on
/// the canvas.
pub(super) fn tooltip(
target: &impl LayoutTarget,
index: usize,
region: &RegionIr,
tooltip: &Tooltip,
) -> Result<TooltipIr> {
let authored = std::iter::once(tooltip.title.as_str()).chain(tooltip.lines.iter().map(String::as_str));
if authored.clone().any(|text| text.contains('<')) {
return Err(target.layout_err(format!(
"{} has a tooltip with `<`: full-screen tooltips draw plain text, without MiniMessage tags",
region.source
)));
}
if authored.clone().any(|text| text.contains('§')) {
return Err(target.layout_err(format!(
"{} has a tooltip with `§`: full-screen tooltips draw plain text, without formatting codes",
region.source
)));
}
// An embedded newline starts another tooltip line, as in vanilla; the display itself never sees one.
let texts: Vec<&str> = authored.flat_map(|text| text.split('\n')).collect();
let measure = |text: &str| vanilla::visible_width(text, false, vanilla::advance, vanilla::glyph_width);
let text_width = texts.iter().map(|text| measure(text)).max().unwrap_or(0);
let extra = texts.len() as u32 - 1;
let text_height = 8 + if extra > 0 { TITLE_GAP + extra * LINE_PITCH } else { 0 };
let size = Size::new(text_width + 2 * INSET, text_height + 2 * INSET);
let canvas = target.bounds();
if size.width > canvas.width || size.height > canvas.height {
return Err(target.layout_err(format!(
"{} has a {}x{} tooltip, which does not fit the {}x{} canvas",
region.source, size.width, size.height, canvas.width, canvas.height
)));
}
let rect = region.rect;
let (width, height) = (size.width as i32, size.height as i32);
let below = rect.y + rect.height as i32 + GAP;
let above = rect.y - GAP - height;
let y = if below + height <= canvas.height as i32 || above < 0 { below } else { above };
let x = rect.x + (rect.width as i32 - width) / 2;
let panel = Rect::from_parts(
Point::new(x.clamp(0, canvas.width as i32 - width), y.clamp(0, canvas.height as i32 - height)),
size,
);
let lines = texts
.iter()
.enumerate()
.filter(|(_, text)| !text.is_empty())
.map(|(line, text)| {
let top = if line == 0 { 0 } else { TITLE_GAP + line as u32 * LINE_PITCH };
let origin = Point::new(panel.x + INSET as i32, panel.y + (INSET + top) as i32);
SlotIr {
name: format!("{}/tooltip/{line}", region.name),
text: Some(text.to_string()),
rect: Rect::from_parts(origin, Size::new(measure(text), 8)),
align: Align::Left,
color: if line == 0 { TITLE_COLOR } else { LINE_COLOR },
shadow: false,
bold: false,
italic: false,
underlined: false,
strikethrough: false,
obfuscated: false,
font: None,
fit: TextFit::default(),
tint: None,
source: Some(region.source.clone()),
}
})
.collect();
Ok(TooltipIr { region: index, panel, lines })
}
3 changes: 2 additions & 1 deletion crates/window-core/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ mod fullscreen;
mod hud;

pub use fullscreen::{
ControlEntry, CursorEntry, FullscreenCanvasEntry, FullscreenEntry, FullscreenSurfaceEntry, HorseEntry,
ControlEntry, ControlTooltipEntry, CursorEntry, FullscreenCanvasEntry, FullscreenEntry, FullscreenSurfaceEntry,
HorseEntry,
};
pub use hud::{HudEntry, HudShaderEntry, HudSurfaceEntry};

Expand Down
14 changes: 14 additions & 0 deletions crates/window-core/src/manifest/fullscreen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,25 @@ pub struct ControlEntry {
/// while the cursor is over the control. Present when [`Self::hover`] is set.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outline: Option<String>,
/// The control's tooltip, which the runtime sends while the cursor is over the control.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tooltip: Option<ControlTooltipEntry>,
/// The authored element this control comes from, such as ``region `buy` ``, for diagnostics.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}

/// A full-screen control's tooltip: a panel beside the control and its plain-text lines.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlTooltipEntry {
/// The panel, net-zero from the line origin.
pub panel: String,
/// The marker color [`Self::panel`] is sent in.
pub marker: String,
/// The title, then each line, as static labels sent in their `shader_marker`.
pub lines: Vec<SlotEntry>,
}

/// The full-screen canvas contract of [`crate::fullscreen`], which the runtime and the generated shaders share.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FullscreenCanvasEntry {
Expand Down
Loading
Loading