use crate::actions::{DraftKey, Drafts};
use crate::composer::{self, Caret, ComposerRam, QueueRow, Step};
use crate::inboxview::InboxEntry;
use crate::jsonview::{GLYPH_COLLAPSED, GLYPH_EXPANDED, toggle_path};
use crate::theme;
const BOX_HINT: &str = "Say what you want done. Enter sends it — as a message to the selected \
conversation, or as a new conversation when nothing is selected. Shift+Enter \
inserts a newline instead. A draft starting with `/` is a command instead — \
type `/` alone to see them all, `//` to say a literal slash. ↑ on the box's \
top line brings back what you already said here, newest first; ↓ on its \
bottom line comes forward again, and past the newest hands your draft back.";
const FOLD_HOVER: &str = "Fold this pending message open or shut. Either way it stays in the \
inbox: everything below the line enters the next prompt when delivery drains it. \
No key of its own: Tab reaches it, Space presses it.";
pub(super) struct QueueCtx {
pub key: DraftKey,
pub agent_id: Option<String>,
pub pending: Vec<InboxEntry>,
pub hint: String,
pub prompts: Vec<String>,
pub cap: f32,
}
pub(super) fn region(
ui: &mut egui::Ui,
ram: &mut ComposerRam,
drafts: &mut Drafts,
ctx: &QueueCtx,
titles: &crate::nav::convs::Titles,
) -> egui::Response {
let ComposerRam {
folds,
snap,
recall,
caret,
} = ram;
let now = ui.input(|i| i.time);
snap.observe(&ctx.key, ctx.pending.len(), now);
let desired = snap.desired(ctx.cap, now);
let settled = snap.settled();
let agent = ctx.agent_id.clone().unwrap_or_default();
let queue = composer::rows(&agent, &ctx.pending, titles, folds);
let (rect, _space) = ui.allocate_exact_size(
egui::vec2(ui.available_width(), desired),
egui::Sense::hover(),
);
let mut region = ui.new_child(
egui::UiBuilder::new()
.max_rect(rect)
.layout(egui::Layout::top_down(egui::Align::Min)),
);
let mut body = |ui: &mut egui::Ui| {
for row in &queue {
pending_row(ui, row, folds);
}
input_box(ui, recall, caret, drafts, ctx)
};
let (edit, painted) = if settled > desired + 0.5 {
crate::tail::scroll(&mut region, true, body)
} else {
region.add_space((desired - settled).max(0.0));
let top = region.cursor().top();
let edit = body(&mut region);
(edit, (region.min_rect().bottom() - top).max(0.0))
};
snap.settle(painted);
if snap.active(now) {
ui.ctx().request_repaint();
}
edit
}
fn pending_row(ui: &mut egui::Ui, row: &QueueRow, folds: &mut std::collections::HashSet<String>) {
ui.scope(|ui| {
ui.set_opacity(theme::tone_solidity(row.tone));
row_body(ui, row, folds);
});
}
fn row_body(ui: &mut egui::Ui, row: &QueueRow, folds: &mut std::collections::HashSet<String>) {
ui.horizontal(|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
theme::role_stripe(ui, Some(row.role));
let glyph = if row.expanded {
GLYPH_EXPANDED
} else {
GLYPH_COLLAPSED
};
let hit = ui
.add(
egui::Label::new(egui::RichText::new(glyph).monospace())
.sense(egui::Sense::click()),
)
.on_hover_text(FOLD_HOVER);
if hit.clicked() {
toggle_path(folds, &row.key);
}
ui.colored_label(theme::BRAZEN, &row.header);
if !row.expanded {
ui.weak(&row.preview);
}
});
if row.expanded {
ui.scope(|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
ui.label(&row.body);
});
}
}
fn input_box(
ui: &mut egui::Ui,
recall: &mut composer::Recall,
caret: &mut Caret,
drafts: &mut Drafts,
ctx: &QueueCtx,
) -> egui::Response {
let id = egui::Id::new("inbox-composer-box");
let mut buffer = drafts.text(&ctx.key);
recall.settle(&buffer, &ctx.prompts);
let mut stepped = false;
if ui.memory(|m| m.has_focus(id)) {
for (key, dir) in [
(egui::Key::ArrowUp, Step::Back),
(egui::Key::ArrowDown, Step::Forward),
] {
if !ui.input(|i| i.modifiers.is_none() && i.key_pressed(key)) {
continue;
}
if let Some(text) = recall.step(dir, *caret, &buffer, &ctx.prompts) {
ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, key));
buffer = text;
stepped = true;
}
}
}
let out = egui::TextEdit::multiline(&mut buffer)
.id(id)
.desired_rows(1)
.desired_width(f32::INFINITY)
.return_key(egui::KeyboardShortcut::new(
egui::Modifiers::SHIFT,
egui::Key::Enter,
))
.hint_text(&ctx.hint)
.show(ui);
let rows = out.galley.rows.len();
*caret = if stepped {
park_at_end(ui, id, out.state, buffer.chars().count());
Caret {
row: rows.saturating_sub(1),
rows,
}
} else {
Caret {
row: out.cursor_range.map_or(0, |r| r.primary.rcursor.row),
rows,
}
};
drafts.set(ctx.key.clone(), buffer);
out.response.on_hover_text(BOX_HINT)
}
fn park_at_end(
ui: &egui::Ui,
id: egui::Id,
mut state: egui::text_edit::TextEditState,
chars: usize,
) {
state
.cursor
.set_char_range(Some(egui::text::CCursorRange::one(
egui::text::CCursor::new(chars),
)));
state.store(ui.ctx(), id);
}