use concinnity_core::components::TextAlign;
use concinnity_core::ecs::World;
use super::registry::PanelKey;
use super::shader_diagnostics::{Status, Tone};
use super::shader_list_panel::tone_color;
use super::shader_reference_panel::{self as reference, COLUMN_W, RefView};
use crate::editor::hud_ids::{hud_ids, panel_base};
use crate::editor::text_area::TextArea;
use crate::editor::text_area::layout::{self, Geometry, Metrics, TextAreaIds, TextAreaView};
use crate::editor::text_area::markers::{GutterMarker, Severity};
use crate::editor::theme;
use crate::editor::widget::{self, place_rounded, point_in};
hud_ids! {
base: panel_base(PanelKey::ShaderSource);
sprites: [
pub(crate) PANEL_BG,
CLOSE_BG,
SAVE_BG,
REF_BG,
..AREA.sprite_ids(),
..reference::ids().sprites.iter().copied(),
];
labels: [
TITLE_LABEL,
CLOSE_LABEL,
SAVE_LABEL,
PATH_LABEL,
STATUS_LABEL,
DIRTY_LABEL,
REF_LABEL,
..reference::ids().labels.iter().copied(),
];
code_labels: [
..AREA.code_label_ids(),
..reference::ids().code_labels.iter().copied(),
];
blocks: [AREA_BASE: TextAreaIds::SPAN, pub(super) REFERENCE_BASE: reference::IDS_SPAN];
}
pub(crate) const AREA: TextAreaIds = TextAreaIds::new(AREA_BASE);
const SOURCE_W: f32 = 660.0;
const PAD: f32 = 10.0;
const HEADER_H: f32 = 32.0;
const STATUS_H: f32 = 2.0 * widget::LINE_H + 6.0;
const BTN_W: f32 = 70.0;
const REF_BTN_W: f32 = 90.0;
const DIRTY_W: f32 = 70.0;
const DEFAULT_ROWS: usize = 24;
const CHROME_H: f32 = widget::TITLE_H + HEADER_H + STATUS_H;
const BTN_TINT: [f32; 4] = [0.22, 0.40, 0.56, 1.0];
const BTN_TINT_HOVER: [f32; 4] = [0.28, 0.48, 0.66, 1.0];
const PATH_COLOR: [f32; 3] = [0.62, 0.66, 0.76];
pub(crate) struct SourceView<'a> {
pub title: &'a str,
pub path: &'a str,
pub area: &'a TextArea,
pub focus: bool,
pub status: Option<&'a Status>,
pub markers: &'a [GutterMarker],
pub mouse: [f32; 2],
pub reference: Option<RefView<'a>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SourceAction {
Text,
Save,
Status,
ToggleReference,
Reference(usize),
Consume,
}
pub(crate) fn default_origin(vw: f32) -> [f32; 2] {
[
(vw * 0.5 - SOURCE_W).max(8.0),
crate::editor::hud::body_top() + 8.0,
]
}
fn reference_w(reference: bool) -> f32 {
if reference { COLUMN_W + PAD } else { 0.0 }
}
pub(crate) fn size(reference: bool) -> [f32; 2] {
[
SOURCE_W + reference_w(reference),
CHROME_H + layout::height_for_rows(DEFAULT_ROWS, Metrics::code().line_h) + PAD,
]
}
pub(crate) fn max_size() -> [f32; 2] {
let rows = layout::height_for_rows(layout::ROW_POOL, Metrics::code().line_h);
[f32::INFINITY, CHROME_H + rows + PAD]
}
fn save_rect(o: [f32; 2], w: f32) -> [f32; 4] {
[
o[0] + w - PAD - BTN_W,
o[1] + widget::TITLE_H + 4.0,
BTN_W,
HEADER_H - 8.0,
]
}
fn reference_button_rect(o: [f32; 2], w: f32) -> [f32; 4] {
let save = save_rect(o, w);
[save[0] - PAD - REF_BTN_W, save[1], REF_BTN_W, save[3]]
}
fn status_rect(o: [f32; 2], w: f32) -> [f32; 4] {
[
o[0] + PAD,
o[1] + widget::TITLE_H + HEADER_H + 1.0,
(w - 2.0 * PAD).max(0.0),
STATUS_H - 4.0,
]
}
pub(crate) fn area_rect(o: [f32; 2], s: [f32; 2], reference: bool) -> [f32; 4] {
let top = o[1] + CHROME_H;
[
o[0] + PAD,
top,
(s[0] - 2.0 * PAD - reference_w(reference)).max(0.0),
(o[1] + s[1] - PAD - top).max(0.0),
]
}
pub(crate) fn reference_rect(o: [f32; 2], s: [f32; 2]) -> [f32; 4] {
let text = area_rect(o, s, true);
[o[0] + s[0] - PAD - COLUMN_W, text[1], COLUMN_W, text[3]]
}
pub(crate) fn area_geometry(
o: [f32; 2],
s: [f32; 2],
reference: bool,
area: &TextArea,
m: Metrics,
) -> Geometry {
layout::geometry(area_rect(o, s, reference), m, area.line_count())
}
pub(crate) fn cursor_over_area(
mx: f32,
my: f32,
o: [f32; 2],
s: [f32; 2],
reference: bool,
) -> bool {
point_in(mx, my, area_rect(o, s, reference))
}
pub(crate) fn cursor_over_reference(mx: f32, my: f32, o: [f32; 2], s: [f32; 2]) -> bool {
point_in(mx, my, reference_rect(o, s))
}
pub(crate) fn hit_test(
mx: f32,
my: f32,
o: [f32; 2],
s: [f32; 2],
reference: bool,
) -> Option<SourceAction> {
if point_in(mx, my, save_rect(o, s[0])) {
return Some(SourceAction::Save);
}
if point_in(mx, my, reference_button_rect(o, s[0])) {
return Some(SourceAction::ToggleReference);
}
if point_in(mx, my, status_rect(o, s[0])) {
return Some(SourceAction::Status);
}
if point_in(mx, my, area_rect(o, s, reference)) {
return Some(SourceAction::Text);
}
if reference && let Some(slot) = reference::slot_at(reference_rect(o, s), mx, my) {
return Some(SourceAction::Reference(slot));
}
point_in(mx, my, widget::outer_rect(o, s)).then_some(SourceAction::Consume)
}
fn status_line(view: &SourceView, g: &Geometry, reference: [f32; 4]) -> Option<Status> {
let area_view = TextAreaView {
area: view.area,
focused: view.focus,
markers: view.markers,
mouse: view.mouse,
};
match layout::hovered_marker(&area_view, g) {
Some(m) => {
let tone = match m.severity {
Severity::Error => Tone::Error,
Severity::Warning => Tone::Warning,
};
Some(Status::new(
format!("line {}: {}", m.line + 1, m.message),
tone,
))
}
None => view
.reference
.as_ref()
.and_then(|r| reference::hovered(r, reference))
.and_then(|row| row.describe())
.map(|text| Status::new(text, Tone::Info))
.or_else(|| view.status.cloned()),
}
}
pub(crate) fn place(
world: &mut World,
view: Option<&SourceView>,
o: [f32; 2],
s: [f32; 2],
m: Metrics,
) {
let Some(view) = view else {
ids().hide(world);
return;
};
let w = s[0];
let dirty = view.area.is_dirty();
widget::place_panel(world, PANEL_BG, widget::outer_rect(o, s));
let title = widget::title_rect(o, w);
let heading = match dirty {
true => format!("{} *", view.title),
false => view.title.to_string(),
};
widget::place_heading(world, TITLE_LABEL, title, &heading);
let close_hover = point_in(view.mouse[0], view.mouse[1], widget::close_rect(title));
widget::place_close(world, CLOSE_BG, CLOSE_LABEL, title, close_hover);
place_header(world, view, o, w, dirty);
let shows_reference = view.reference.is_some();
let g = area_geometry(o, s, shows_reference, view.area, m);
let reference_at = reference_rect(o, s);
let status = status_line(view, &g, reference_at);
let (text, tone) = status
.as_ref()
.map_or(("", Tone::Info), |s| (s.text.as_str(), s.tone));
widget::place_message(
world,
STATUS_LABEL,
status_rect(o, w),
text,
tone_color(tone),
status.is_some(),
);
let area_view = TextAreaView {
area: view.area,
focused: view.focus,
markers: view.markers,
mouse: view.mouse,
};
layout::place(world, AREA, Some(&area_view), &g);
reference::place(world, view.reference.as_ref(), reference_at);
}
fn place_header(world: &mut World, view: &SourceView, o: [f32; 2], w: f32, dirty: bool) {
let y = o[1] + widget::TITLE_H + HEADER_H * 0.5 - theme::TEXT_HALF;
let save = save_rect(o, w);
let toggle = reference_button_rect(o, w);
let path_w = (toggle[0] - DIRTY_W - PAD - (o[0] + PAD)).max(0.0);
widget::place_message(
world,
PATH_LABEL,
[o[0] + PAD, y, path_w, widget::LINE_H],
view.path,
PATH_COLOR,
true,
);
if let Some(l) = widget::label_mut(world, DIRTY_LABEL) {
l.x = toggle[0] - PAD;
l.y = y;
l.align = TextAlign::Right;
l.color = theme::LOG_WARN;
l.visible = dirty;
l.content = "unsaved".to_string();
}
let open = view.reference.is_some();
let toggle_tint = match (open, point_in(view.mouse[0], view.mouse[1], toggle)) {
(true, _) => theme::ACCENT_TINT,
(false, true) => theme::HOVER_TINT,
(false, false) => theme::BUTTON_TINT,
};
place_rounded(
world,
REF_BG,
toggle,
toggle_tint,
theme::CONTROL_RADIUS,
true,
);
widget::place_center_label(
world,
REF_LABEL,
[
toggle[0] + toggle[2] * 0.5,
toggle[1] + toggle[3] * 0.5 - theme::TEXT_HALF,
],
"Reference",
[1.0, 1.0, 1.0],
true,
);
let hover = point_in(view.mouse[0], view.mouse[1], save);
let tint = if hover { BTN_TINT_HOVER } else { BTN_TINT };
place_rounded(world, SAVE_BG, save, tint, theme::CONTROL_RADIUS, true);
widget::place_center_label(
world,
SAVE_LABEL,
[
save[0] + save[2] * 0.5,
save[1] + save[3] * 0.5 - theme::TEXT_HALF,
],
"Save",
[1.0, 1.0, 1.0],
true,
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::editor::panels::shader_reference::{RefRow, Reference};
use concinnity_core::components::TextLabel;
use concinnity_core::ecs::asset_id::AssetId;
use concinnity_core::render::shader_programs::vocabulary::ENTRIES;
const M: Metrics = Metrics {
line_h: 20.0,
advance: 8.0,
text_px: 14.0,
label_scale: 0.7,
};
fn injected_world() -> World {
ids().test_world()
}
fn view<'a>(
area: &'a TextArea,
status: Option<&'a Status>,
markers: &'a [GutterMarker],
) -> SourceView<'a> {
SourceView {
title: "water fragment",
path: "shaders/water.hlsl",
area,
focus: true,
status,
markers,
mouse: [0.0, 0.0],
reference: None,
}
}
fn label(world: &World, id: AssetId) -> TextLabel {
world.get_by_id::<TextLabel>(id).cloned().unwrap()
}
#[test]
fn hit_test_resolves_save_status_and_text() {
let o = [40.0, 40.0];
let s = size(false);
let at = |r: [f32; 4]| (r[0] + 5.0, r[1] + 5.0);
let (x, y) = at(save_rect(o, s[0]));
assert_eq!(hit_test(x, y, o, s, false), Some(SourceAction::Save));
let (x, y) = at(status_rect(o, s[0]));
assert_eq!(hit_test(x, y, o, s, false), Some(SourceAction::Status));
let (x, y) = at(area_rect(o, s, false));
assert_eq!(hit_test(x, y, o, s, false), Some(SourceAction::Text));
assert_eq!(
hit_test(o[0] + 2.0, o[1] + widget::TITLE_H + 4.0, o, s, false),
Some(SourceAction::Consume)
);
assert_eq!(hit_test(5000.0, 5000.0, o, s, false), None);
}
#[test]
fn unsaved_edits_mark_the_title_and_header() {
let mut world = injected_world();
let mut area = TextArea::from_text("float4 shade() {}");
place(
&mut world,
Some(&view(&area, None, &[])),
[20.0, 20.0],
size(false),
M,
);
assert_eq!(label(&world, TITLE_LABEL).content, "water fragment");
assert!(!label(&world, DIRTY_LABEL).visible);
area.type_char('x');
place(
&mut world,
Some(&view(&area, None, &[])),
[20.0, 20.0],
size(false),
M,
);
assert_eq!(label(&world, TITLE_LABEL).content, "water fragment *");
assert!(label(&world, DIRTY_LABEL).visible);
}
#[test]
fn the_status_line_takes_its_tone() {
let mut world = injected_world();
let area = TextArea::from_text("a");
let status = Status::new("line 1: bad", Tone::Error);
place(
&mut world,
Some(&view(&area, Some(&status), &[])),
[20.0, 20.0],
size(false),
M,
);
let l = label(&world, STATUS_LABEL);
assert!(l.visible && l.content == "line 1: bad");
assert_eq!(l.color, theme::LOG_ERROR);
}
#[test]
fn a_hovered_marker_reads_out_on_the_status_line() {
let mut world = injected_world();
let area = TextArea::from_text("a\nb\nc");
let markers = [GutterMarker {
line: 1,
column: 0,
severity: Severity::Warning,
message: "unused".to_string(),
}];
let o = [20.0, 20.0];
let r = area_rect(o, size(false), false);
let mut v = view(&area, None, &markers);
v.mouse = [r[0] + 2.0, r[1] + M.line_h * 1.5];
place(&mut world, Some(&v), o, size(false), M);
let l = label(&world, STATUS_LABEL);
assert_eq!(l.content, "line 2: unused");
assert_eq!(l.color, theme::LOG_WARN);
}
fn reference_rows() -> Vec<RefRow<'static>> {
Reference::default().rows(ENTRIES)
}
#[test]
fn the_reference_column_widens_the_panel_beside_the_text() {
let o = [20.0, 20.0];
let s = size(true);
assert_eq!(s[0], size(false)[0] + COLUMN_W + PAD);
let text = area_rect(o, s, true);
let column = reference_rect(o, s);
assert_eq!(
text[2],
area_rect(o, size(false), false)[2],
"the text keeps its width"
);
assert!(
text[0] + text[2] <= column[0],
"the column sits right of the text"
);
assert_eq!((column[1], column[3]), (text[1], text[3]));
}
#[test]
fn hit_test_resolves_the_toggle_and_reference_rows() {
let o = [40.0, 40.0];
let s = size(true);
let t = reference_button_rect(o, s[0]);
assert_eq!(
hit_test(t[0] + 2.0, t[1] + 2.0, o, s, true),
Some(SourceAction::ToggleReference)
);
let c = reference_rect(o, s);
assert_eq!(
hit_test(c[0] + 5.0, c[1] + 30.0, o, s, true),
Some(SourceAction::Reference(1))
);
assert_eq!(
hit_test(c[0] + 5.0, c[1] + 30.0, o, s, false),
Some(SourceAction::Text),
"a hidden column's room is the text's"
);
}
#[test]
fn a_hovered_reference_name_reads_out_on_the_status_line() {
let mut world = injected_world();
let area = TextArea::from_text("a");
let rows = reference_rows();
let o = [20.0, 20.0];
let s = size(true);
let c = reference_rect(o, s);
let status = Status::new("compiled", Tone::Info);
let mut v = view(&area, Some(&status), &[]);
let mouse = [c[0] + 5.0, c[1] + 30.0];
v.mouse = mouse;
v.reference = Some(RefView {
rows: &rows,
scroll: 0,
mouse,
});
place(&mut world, Some(&v), o, s, M);
let l = label(&world, STATUS_LABEL);
assert!(
l.content
.starts_with("float4 shade_surface(VertexOut v, GpuObjectData od): "),
"{}",
l.content
);
assert!(label(&world, REF_LABEL).visible);
}
}