use egui::{Popup, PopupCloseBehavior, RichText, TextEdit, Ui};
use egui_phosphor_icons::icons;
use gled_model::asset::midi_controller::MidiModeHighlight;
use strum::IntoEnumIterator;
use super::intent::Intent;
use super::list_state::ModeFilter;
use super::rule_card::{Cards, group_card, rule_card};
use super::rules::{ControlKind, Plain, RuleGroup, action_sentence, source_sentence, text};
pub(crate) fn rule_list(ui: &mut Ui, cards: &mut Cards, groups: &[RuleGroup]) {
let mapping = cards.mapping;
if cards.rules.is_empty() {
ui.add_space(20.0);
ui.vertical_centered(|ui| {
ui.heading("No rules yet");
ui.label("Press \"Learn\" above, then move a control on your controller.");
ui.label(
"Each control you move gets a rule; pick what it does from the list that opens.",
);
});
return;
}
let query = cards.state.find.trim().to_lowercase();
let mut sections: Vec<Option<u8>> = vec![None];
sections.extend(
(0..mapping.modes.len())
.filter_map(|mode| u8::try_from(mode).ok())
.map(Some),
);
let mut shown = 0;
for section in sections {
let visible = match cards.state.filter {
ModeFilter::All => true,
ModeFilter::Everywhere => section.is_none(),
ModeFilter::Mode(mode) => section == Some(mode),
};
if !visible {
continue;
}
let items = section_items(cards, groups, section, &query);
if items.is_empty() && section.is_none() && !mapping.modes.is_empty() {
continue;
}
section_header(ui, cards, section, items.len());
if let Some(mode) = section {
mode_setup(ui, cards, mode);
}
for item in items {
match item {
Item::Group(group) => group_card(ui, cards, &groups[group]),
Item::Rule(rule) => rule_card(ui, cards, rule),
}
shown += 1;
}
if section.is_some() && !query.is_empty() {
continue;
}
if ui
.small_button(super::iconized(
ui,
icons::PLUS,
" Add a rule here: move a control",
))
.clicked()
{
cards.state.learn_request = Some(super::list_state::LearnRequest::NewControls);
cards.state.filter = match section {
Some(mode) => ModeFilter::Mode(mode),
None if mapping.modes.is_empty() => ModeFilter::All,
None => ModeFilter::Everywhere,
};
}
ui.add_space(8.0);
}
if shown == 0 && !query.is_empty() {
ui.weak(format!("No rule mentions \"{}\"", cards.state.find.trim()));
}
}
enum Item {
Group(usize),
Rule(usize),
}
fn section_items(
cards: &Cards,
groups: &[RuleGroup],
section: Option<u8>,
query: &str,
) -> Vec<Item> {
let grouped: Vec<usize> = groups
.iter()
.flat_map(|group| group.rules.iter().copied())
.collect();
let mut items: Vec<(usize, Item)> = Vec::new();
for (index, group) in groups.iter().enumerate() {
let first = &cards.rules[group.rules[0]];
if first.mode == section && group.rules.iter().any(|&rule| matches(cards, rule, query)) {
items.push((first.control, Item::Group(index)));
}
}
for (index, rule) in cards.rules.iter().enumerate() {
if rule.mode == section && !grouped.contains(&index) && matches(cards, index, query) {
items.push((rule.control, Item::Rule(index)));
}
}
items.sort_by_key(|(control, _)| {
let kind = match cards.controls[*control].kind {
ControlKind::Encoder => 0,
ControlKind::Knob => 1,
ControlKind::Fader => 2,
ControlKind::Button | ControlKind::Pad => 3,
};
(kind, *control)
});
items.into_iter().map(|(_, item)| item).collect()
}
fn matches(cards: &Cards, rule: usize, query: &str) -> bool {
if query.is_empty() {
return true;
}
let names_match = cards
.controls
.iter()
.any(|control| control.name.to_lowercase().contains(query));
let rule = &cards.rules[rule];
if names_match {
return cards.controls[rule.control]
.name
.to_lowercase()
.contains(query);
}
let mapping = cards.mapping;
let mut words = String::new();
if let Some(index) = rule.press.or(rule.release) {
words += &text(&action_sentence(
&mapping.input_bindings[index].action,
&mapping.modes,
&Plain,
));
}
for &output in &rule.feedback {
if let Some(source) = mapping.output_bindings[output].kind.source() {
words += &text(&source_sentence(source, &mapping.modes, &Plain));
}
}
words.to_lowercase().contains(query)
}
fn section_header(ui: &mut Ui, cards: &Cards, section: Option<u8>, count: usize) {
let mapping = cards.mapping;
ui.horizontal(|ui| {
match section {
None if mapping.modes.is_empty() => {
ui.label(RichText::new("Rules").strong());
}
None => {
ui.label(RichText::new("In all modes").strong());
ui.weak("always active, whatever mode the controller is in");
}
Some(mode) => {
let name = mapping
.modes
.get(usize::from(mode))
.map_or("?", |mode| mode.name.as_str());
ui.label(RichText::new(format!("Mode {name}")).strong());
let switches: Vec<&str> = cards
.rules
.iter()
.filter(|rule| {
rule.press.is_some_and(|press| {
mapping.input_bindings[press].action
== gled_model::asset::midi_controller::MidiInputAction::SelectMode {
mode,
}
})
})
.map(|rule| cards.controls[rule.control].name.as_str())
.collect();
if switches.is_empty() {
ui.weak("no button switches to it yet");
} else {
ui.weak(format!("{} switches to it", switches.join(", ")));
}
}
}
ui.weak(format!("· {count}"));
});
}
fn mode_setup(ui: &mut Ui, cards: &mut Cards, mode: u8) {
let mapping = cards.mapping;
let Some(entry) = mapping.modes.get(usize::from(mode)) else {
return;
};
ui.horizontal_wrapped(|ui| {
if ui
.small_button("Choose the button that switches to it…")
.on_hover_text(
"Then press the button on your controller; it gets a light showing the mode",
)
.clicked()
{
cards.state.learn_request = Some(super::list_state::LearnRequest::ModeSwitch(mode));
}
ui.weak("· highlight in gled when switching here:");
let label = |highlight: Option<MidiModeHighlight>| {
highlight.map_or_else(
|| "nothing".to_owned(),
|highlight| highlight.to_string().to_lowercase(),
)
};
let response = super::widgets::token::token(
ui,
&label(entry.highlight),
super::widgets::token::token_color(super::rules::TokenKind::Mode),
);
Popup::menu(&response)
.close_behavior(PopupCloseBehavior::CloseOnClickOutside)
.show(|ui| {
for option in std::iter::once(None).chain(MidiModeHighlight::iter().map(Some)) {
if ui
.selectable_label(entry.highlight == option, label(option))
.clicked()
{
cards.intents.push(Intent::SetHighlight {
mode,
highlight: option,
});
ui.close();
}
}
});
});
}
pub(crate) fn mode_chips(ui: &mut Ui, cards: &mut Cards) {
let mapping = cards.mapping;
let count = |mode: Option<Option<u8>>| {
cards
.rules
.iter()
.filter(|rule| mode.is_none_or(|mode| rule.mode == mode))
.count()
};
ui.horizontal_wrapped(|ui| {
let filter = &mut cards.state.filter;
ui.selectable_value(filter, ModeFilter::All, format!("All {}", count(None)));
if !mapping.modes.is_empty() {
ui.selectable_value(
filter,
ModeFilter::Everywhere,
format!("In all modes {}", count(Some(None))),
)
.on_hover_text("Rules that work whatever mode the controller is in");
for (index, mode) in mapping.modes.iter().enumerate() {
let Ok(index) = u8::try_from(index) else {
continue;
};
ui.selectable_value(
filter,
ModeFilter::Mode(index),
format!("{} {}", mode.name, count(Some(Some(index)))),
);
}
}
let add = ui
.button(super::iconized(ui, icons::PLUS, " Mode"))
.on_hover_text(
"Modes let the same controls do different things; a button switches between them",
);
Popup::menu(&add)
.close_behavior(PopupCloseBehavior::CloseOnClickOutside)
.show(|ui| {
let id = ui.make_persistent_id("new mode name");
let mut name: String = ui.data(|data| data.get_temp(id)).unwrap_or_default();
ui.label("Name of the new mode");
let field = ui.add(TextEdit::singleline(&mut name).hint_text("e.g. Grid"));
field.request_focus();
let done = ui.input(|input| input.key_pressed(egui::Key::Enter));
if (done || ui.button("Add mode").clicked()) && !name.trim().is_empty() {
cards.intents.push(Intent::AddMode(name.trim().to_owned()));
cards.state.filter =
ModeFilter::Mode(u8::try_from(mapping.modes.len()).unwrap_or(u8::MAX));
name.clear();
ui.close();
}
ui.data_mut(|data| data.insert_temp(id, name));
});
ui.add_space(12.0);
let find = ui.add(
TextEdit::singleline(&mut cards.state.find)
.hint_text("Find rules (/)")
.desired_width(200.0)
.id_salt("find rules"),
);
if std::mem::take(&mut cards.state.focus_find) {
find.request_focus();
}
});
}