use super::super::ui_spec::{centered_label, label_value, sprite};
use super::rows::{
BodyRow, SettingsRow, option_select_row, settings_body_rows, settings_tabs, slider_row,
};
use super::{TOP_MARGIN_FRAC, cursor_sprite, opaque};
use crate::authoring::registry::build_only::MainMenu;
use crate::authoring::spec::{asset, spec_to_value};
const AVG_ADVANCE_RATIO: f32 = 0.5;
const SETTINGS_ROW_WIDTH_MULT: f32 = 1.85;
const SETTINGS_ROW_SCALE: f32 = 0.6;
const SETTINGS_SIDE_MARGIN: f32 = 90.0;
const SETTINGS_CONTROL_FRAC: f32 = 0.42;
const SETTINGS_CONTROL_WIDTH: f32 = 360.0;
const SETTINGS_ROW_BG: [f32; 4] = [0.10, 0.13, 0.24, 0.55];
const SETTINGS_HEADER_BG: [f32; 4] = [0.16, 0.20, 0.34, 0.70];
const SETTINGS_ROW_PAD: f32 = 18.0;
const VISIBLE_SETTINGS_ROWS: usize = 6;
const SCROLLBAR_GAP: f32 = 12.0;
const SCROLLBAR_WIDTH: f32 = 8.0;
const BACK_GAP: f32 = 24.0;
pub(super) fn emit_settings_tab(
menu_name: &str,
active: &str,
style: &MainMenu,
font: &str,
win_w: f32,
win_h: f32,
font_px: f32,
) -> Vec<serde_json::Value> {
let screen = format!("{}_settings_{}", menu_name, active);
let mut out = Vec::new();
out.push(spec_to_value(&asset::screen(&screen, false)));
if style.dim[3] > 0.0 {
out.push(sprite(
&format!("{}_dim", screen),
0.0,
0.0,
win_w,
win_h,
style.dim,
));
}
let pitch = style.button_height + style.row_gap;
let center_x = if style.centered { win_w / 2.0 } else { style.x };
let start_y = if style.centered {
win_h * TOP_MARGIN_FRAC
} else {
style.y
};
let row_y = |i: usize| start_y + i as f32 * pitch;
let text_y = |i: usize, scale: f32| row_y(i) + (style.button_height - font_px * scale) / 2.0;
let row_scale = style.text_scale * SETTINGS_ROW_SCALE;
let (row_x, row_width) = if style.centered {
let w = win_w - 2.0 * SETTINGS_SIDE_MARGIN - SCROLLBAR_GAP - SCROLLBAR_WIDTH;
(SETTINGS_SIDE_MARGIN, w)
} else {
let w = (style.button_width * SETTINGS_ROW_WIDTH_MULT).min(win_w - 80.0);
(center_x - w / 2.0, w)
};
let content_x = row_x + SETTINGS_ROW_PAD;
let content_w = (row_width - 2.0 * SETTINGS_ROW_PAD).max(0.0);
let control_x = (content_x + content_w * SETTINGS_CONTROL_FRAC)
.max(content_x + content_w - SETTINGS_CONTROL_WIDTH);
let tabs = settings_tabs(style.settings_profile);
let tab_scale = style.text_scale * 1.1;
let tab_text_y = text_y(0, tab_scale);
let tab_gap = font_px * AVG_ADVANCE_RATIO * tab_scale * 1.2;
let tab_widths: Vec<f32> = tabs
.iter()
.map(|&(_, label)| text_width(label, font_px, tab_scale))
.collect();
let tabs_total: f32 = tab_widths.iter().sum::<f32>() + tab_gap * (tabs.len() as f32 - 1.0);
let mut tab_x = center_x - tabs_total / 2.0;
for (&(suffix, label), w) in tabs.iter().zip(&tab_widths) {
let is_active = suffix == active;
let color = if is_active {
style.hover_color
} else {
style.text_color
};
let label_name = format!("{}_tab_{}", screen, suffix);
out.push(label_value(
&label_name,
label,
font,
tab_x,
tab_text_y,
color,
tab_scale,
));
if is_active {
let mark_h = (font_px * tab_scale * 0.08).max(2.0);
out.push(sprite(
&format!("{}_tabmark", screen),
tab_x,
tab_text_y + font_px * tab_scale + mark_h,
*w,
mark_h,
opaque(style.hover_color),
));
} else {
out.push(spec_to_value(
&asset::hit_region(
format!("{}_tabbtn_{}", screen, suffix),
[tab_x, row_y(0), *w, style.button_height],
format!("screen:show:{}_settings_{}", menu_name, suffix),
)
.set("label", label_name)
.set("hover_color", style.hover_color)
.set("hover_scale", tab_scale * style.hover_scale),
));
}
tab_x += w + tab_gap;
}
let band_top = row_y(1);
let band_h = VISIBLE_SETTINGS_ROWS as f32 * pitch;
let text_y_at = |y: f32, scale: f32| y + (style.button_height - font_px * scale) / 2.0;
let (body, groups) = settings_body_rows(active, style.settings_profile);
let mut scroll_rows: Vec<serde_json::Value> = Vec::new();
for (j, row) in body.iter().enumerate() {
let base_y = band_top + j as f32 * pitch;
let is_header = matches!(*row, BodyRow::GroupHeader(..));
let bg_name = format!("{}_bg_{}", screen, j);
out.push(sprite(
&bg_name,
row_x,
base_y,
row_width,
style.button_height,
if is_header {
SETTINGS_HEADER_BG
} else {
SETTINGS_ROW_BG
},
));
let (mut elements, group): (Vec<String>, i32) = match *row {
BodyRow::Option(setting, label, group) => {
let name = format!("{}_opt_{}", screen, setting);
out.push(option_select_row(&SettingsRow {
name: &name,
setting,
label,
font,
x: content_x,
y: base_y,
width: content_w,
scale: row_scale,
style,
}));
(
super::super::option_select::element_names(&name, setting),
group,
)
}
BodyRow::Slider(setting, label, group) => {
let name = format!("{}_sld_{}", screen, setting);
out.push(slider_row(&SettingsRow {
name: &name,
setting,
label,
font,
x: content_x,
y: base_y,
width: content_w,
scale: row_scale,
style,
}));
(super::super::slider::element_names(&name), group)
}
BodyRow::Key(action_label, key, idx, group) => {
let name = format!("{}_keyname_{}", screen, idx);
let val = format!("{}_keyval_{}", screen, idx);
out.push(label_value(
&name,
action_label,
font,
content_x,
text_y_at(base_y, row_scale),
style.text_color,
row_scale,
));
out.push(label_value(
&val,
key,
font,
control_x,
text_y_at(base_y, row_scale),
style.text_color,
row_scale,
));
(vec![name, val], group)
}
BodyRow::Rebind(action_label, setting, idx, group) => {
let name = format!("{}_rebind_name_{}", screen, idx);
let val = format!("{}_rebind_val_{}", screen, idx);
out.push(label_value(
&name,
action_label,
font,
content_x,
text_y_at(base_y, row_scale),
style.text_color,
row_scale,
));
out.push(label_value(
&val,
"--",
font,
control_x,
text_y_at(base_y, row_scale),
style.text_color,
row_scale,
));
let ctrl_w = (content_x + content_w - control_x).max(0.0);
out.push(spec_to_value(
&asset::hit_region(
format!("{}_rebind_btn_{}", screen, idx),
[control_x, base_y, ctrl_w, style.button_height],
format!("setting:{}:rebind", setting),
)
.set("label", val.clone())
.set("hover_color", style.hover_color)
.set("hover_scale", row_scale * style.hover_scale),
));
(vec![name, val], group)
}
BodyRow::GroupHeader(gid, title) => {
let collapsed = groups.iter().any(|g| g.gid == gid && g.collapsed);
let header = format!("{}_grphdr_{}", screen, gid);
let header_scale = row_scale * 1.05;
out.push(label_value(
&header,
&format!("{} {}", if collapsed { "+" } else { "-" }, title),
font,
content_x,
text_y_at(base_y, header_scale),
style.hover_color,
header_scale,
));
out.push(spec_to_value(
&asset::hit_region(
format!("{}_grpbtn_{}", screen, gid),
[row_x, base_y, row_width, style.button_height],
format!("group:toggle:{}", gid),
)
.set("label", header.clone())
.set("hover_color", style.hover_color)
.set("hover_scale", header_scale * style.hover_scale),
));
(vec![header], -1)
}
};
elements.insert(0, bg_name);
scroll_rows.push(serde_json::json!({
"elements": elements,
"base_y": base_y,
"height": pitch,
"group": group,
}));
}
let track_x = row_x + row_width + SCROLLBAR_GAP;
let track_tint = [
style.text_color[0],
style.text_color[1],
style.text_color[2],
0.25,
];
out.push(sprite(
&format!("{}_scrolltrack", screen),
track_x,
band_top,
SCROLLBAR_WIDTH,
band_h,
track_tint,
));
out.push(sprite(
&format!("{}_scrollthumb", screen),
track_x,
band_top,
SCROLLBAR_WIDTH,
band_h * 0.4,
opaque(style.hover_color),
));
let scroll_groups: Vec<serde_json::Value> = groups
.iter()
.map(|g| {
serde_json::json!({
"collapsed": g.collapsed,
"header": format!("{}_grphdr_{}", screen, g.gid),
"title": g.title,
})
})
.collect();
out.push(serde_json::json!({
"name": format!("{}_scroll", screen),
"type": "ScrollPanel",
"args": {
"x": row_x, "y": band_top, "width": row_width, "height": band_h,
"rows": scroll_rows,
"groups": scroll_groups,
"thumb": format!("{}_scrollthumb", screen),
"track": format!("{}_scrolltrack", screen),
"track_x": track_x, "track_y": band_top,
"track_w": SCROLLBAR_WIDTH, "track_h": band_h,
}
}));
let back_y = band_top + band_h + BACK_GAP;
let back_label = format!("{}_label_back", screen);
let back_action = if style.settings_back_action.is_empty() {
format!("screen:show:{}", menu_name)
} else {
style.settings_back_action.clone()
};
out.push(centered_label(
&back_label,
"Back",
font,
center_x,
text_y_at(back_y, style.text_scale),
style.text_color,
style.text_scale,
));
out.push(spec_to_value(
&asset::hit_region(
format!("{}_btn_back", screen),
[
center_x - style.button_width / 2.0,
back_y,
style.button_width,
style.button_height,
],
back_action,
)
.set("label", back_label)
.set("hover_color", style.hover_color)
.set("hover_scale", style.text_scale * style.hover_scale),
));
if style.cursor {
out.push(cursor_sprite(&format!("{}_cursor", screen), style));
}
out
}
fn text_width(text: &str, font_px: f32, scale: f32) -> f32 {
text.chars().count() as f32 * font_px * AVG_ADVANCE_RATIO * scale
}