use crate::config_edit::form::{Control, Group, Row};
use crate::theme;
const RAW_ROWS: usize = 6;
pub(super) const NO_READER: &str =
"yog has no reader for this file's shape — edit it as raw text below";
pub(super) fn raw_editor(ui: &mut egui::Ui, text: &mut String) {
ui.add(
egui::TextEdit::multiline(text)
.desired_rows(RAW_ROWS)
.desired_width(f32::INFINITY)
.code_editor(),
)
.on_hover_text(
"The file's whole contents, exactly as they will be written. Editing \
here changes nothing until the pane's own Apply/Send commits it. No key of \
its own: Tab reaches it, then type — inside it Tab indents.",
);
}
const TYPED: &str = "Typed, the whole file is `/config <destination> <text…>`.";
pub(super) fn render(
ui: &mut egui::Ui,
salt: &str,
groups: &[Group],
provider_rows: &[String],
) -> Option<(Row, String)> {
let mut edit = None;
for group in groups {
ui.label(egui::RichText::new(&group.entry).strong());
ui.indent(format!("{salt}-{}", group.entry), |ui| {
for row in &group.rows {
if let Some(value) = control(ui, salt, row, provider_rows) {
edit = Some((row.clone(), value));
}
}
});
}
edit
}
fn control(ui: &mut egui::Ui, salt: &str, row: &Row, provider_rows: &[String]) -> Option<String> {
let mut out = None;
ui.horizontal(|ui| {
ui.label(row.field).on_hover_text(row.help);
let trailing = row.fault.as_ref().map_or(0.0, |_| FAULT_SEAT);
let width = crate::layout::value_width(ui.available_width(), trailing);
out = match row.control {
Control::Provider => provider(ui, salt, row, provider_rows, width),
Control::Number { min, max } => number(ui, row, min, max),
Control::List | Control::Text => scalar(ui, row, width),
};
if let Some(fault) = &row.fault {
ui.colored_label(theme::ICHOR, "⚠").on_hover_text(fault);
}
});
out
}
const FAULT_SEAT: f32 = 24.0;
fn provider(
ui: &mut egui::Ui,
salt: &str,
row: &Row,
provider_rows: &[String],
width: f32,
) -> Option<String> {
let mut chosen = row.value.clone();
let says = format!("{} {TYPED}", row.help);
egui::ComboBox::from_id_salt(format!("{salt}-{}-{}", row.entry, row.field))
.width(width)
.selected_text(&row.value)
.show_ui(ui, |ui| {
if !provider_rows.contains(&row.value) {
ui.selectable_value(&mut chosen, row.value.clone(), &row.value)
.on_hover_text(says.clone());
}
for name in provider_rows {
ui.selectable_value(&mut chosen, name.clone(), name)
.on_hover_text(says.clone());
}
})
.response
.on_hover_text(says);
(chosen != row.value).then_some(chosen)
}
fn number(ui: &mut egui::Ui, row: &Row, min: u64, max: u64) -> Option<String> {
let mut n: u64 = row.value.parse().unwrap_or(min);
ui.add(egui::DragValue::new(&mut n).range(min..=max))
.on_hover_text(format!("{} {TYPED}", row.help))
.changed()
.then(|| n.to_string())
}
fn scalar(ui: &mut egui::Ui, row: &Row, width: f32) -> Option<String> {
let mut value = row.value.clone();
ui.add(egui::TextEdit::singleline(&mut value).desired_width(width))
.on_hover_text(format!("{} {TYPED}", row.help))
.changed()
.then_some(value)
}
#[cfg(test)]
mod tests {
use super::{raw_editor, render};
use crate::config_edit::form::{Control, Group, Row};
fn long_row() -> Vec<Group> {
vec![Group {
entry: "worker".into(),
rows: vec![Row {
entry: "worker".into(),
field: "tools",
control: Control::List,
help: "the tool names this role may call",
value: "bash, read_file, write_file, load_skill, edit_file, list_dir, \
search_files, run_tests, git_status, git_commit, fetch_url, \
dispatch, message"
.into(),
fault: None,
}],
}]
}
fn widest_visible(out: &egui::FullOutput) -> f32 {
let mut widest: f32 = 0.0;
for clipped in &out.shapes {
let mut here = Vec::new();
crate::paint_probe::collect(&clipped.shape, &mut here);
for (_, rect) in here {
let seen = rect.intersect(clipped.clip_rect);
if seen.height() > 0.5 {
widest = widest.max(seen.width());
}
}
}
widest
}
fn value_run(w: f32) -> f32 {
let ctx = egui::Context::default();
let groups = long_row();
let out = ctx.run(crate::paint_probe::screen_sized(w, 600.0), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
render(ui, "t", &groups, &[]);
});
});
widest_visible(&out)
}
#[test]
fn a_value_box_grows_with_the_pane_it_is_in() {
let (narrow, wide, whole) = (value_run(700.0), value_run(2560.0), value_run(6000.0));
assert!(
wide + 1.0 >= whole,
"the value is still cut at a maximized window: {wide} pt of {whole} pt"
);
assert!(
narrow < whole && wide > narrow,
"the value box ignored the pane: {narrow} pt at 700, {wide} pt at 2560, \
{whole} pt laid out whole"
);
}
fn widest(shape: &egui::Shape) -> f32 {
match shape {
egui::Shape::Text(t) => t.galley.size().x,
egui::Shape::Vec(v) => v.iter().map(widest).fold(0.0, f32::max),
_ => 0.0,
}
}
#[test]
fn a_raw_editor_takes_the_pane_width() {
let mut text = format!("key = \"{}\"", "x".repeat(400));
let ctx = egui::Context::default();
let out = ctx.run(crate::paint_probe::screen(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| raw_editor(ui, &mut text));
});
let w = out
.shapes
.iter()
.map(|c| widest(&c.shape))
.fold(0.0, f32::max);
assert!(
w > 500.0,
"the raw text laid out {w} pt wide — back in the fixed column"
);
}
}