From 2aa53f1e851078b567c05e267b0fb526ad3bd459 Mon Sep 17 00:00:00 2001 From: trintlermint Date: Sun, 5 Apr 2026 22:37:34 +0200 Subject: [PATCH 1/4] refac: change option.on_enter and extract menu to shared lambda --- src/screens/menu.cpp | 216 ++++++++++++++++++++++--------------------- 1 file changed, 111 insertions(+), 105 deletions(-) diff --git a/src/screens/menu.cpp b/src/screens/menu.cpp index 1dcde8b..8744193 100644 --- a/src/screens/menu.cpp +++ b/src/screens/menu.cpp @@ -21,6 +21,113 @@ ftxui::Component make_menu_screen(AppState& state) "Manual", }; + auto activate_menu = [&] { + switch (state.menu_selected) + { + case 0: + if (state.questions.empty()) + { + state.status_message = "No questions loaded."; + } + else if (state.loaded_files.size() <= 1) + { + state.start_quiz(); + } + else + { + state.quiz_setup_phase = 0; + state.quiz_setup_cursor = 0; + state.quiz_file_included.assign(state.loaded_files.size(), true); + state.quiz_file_order.clear(); + state.current_screen = AppScreen::QUIZ_SETUP; + } + break; + case 1: + state.reset_add_form(); + state.current_screen = state.route_to(AppScreen::ADD_QUESTION); + break; + case 2: + if (state.questions.empty()) + state.status_message = "No questions to remove."; + else + { + state.remove_question_idx = 0; + state.current_screen = state.route_to(AppScreen::REMOVE_QUESTION); + } + break; + case 3: + if (state.questions.empty()) + state.status_message = "No questions available."; + else + { + state.select_question_idx = 0; + state.select_new_answer = 0; + state.change_answer_phase = 0; + state.current_screen = state.route_to(AppScreen::CHANGE_ANSWER); + } + break; + case 4: + if (state.questions.empty()) + state.status_message = "No questions available."; + else + { + state.edit_choice_question_idx = 0; + state.edit_choice_choice_idx = 0; + state.edit_choice_phase = 0; + state.current_screen = state.route_to(AppScreen::EDIT_CHOICE); + } + break; + case 5: + if (state.questions.empty()) + state.status_message = "No questions available."; + else + { + state.list_selected = 0; + state.current_screen = state.route_to(AppScreen::LIST_QUESTIONS); + } + break; + case 6: + { + state.current_screen = state.route_to(AppScreen::SET_METADATA); + if (state.current_screen == AppScreen::SET_METADATA) + { + if (state.target_file >= 0 && state.target_file < static_cast(state.loaded_files.size())) + { + auto& lf = state.loaded_files[state.target_file]; + state.meta_name_text = lf.name; + state.meta_author_text = lf.author; + } + else + { + state.meta_name_text = state.quiz_name; + state.meta_author_text = state.quiz_author; + } + } + break; + } + case 7: + if (state.loaded_files.empty()) + { + state.status_message = "No file loaded to save."; + break; + } + state.current_screen = state.route_to(AppScreen::SAVE_CONFIRM); + if (state.current_screen == AppScreen::SAVE_CONFIRM) + state.compute_diff(state.target_file); + break; + case 8: + state.load_path_text.clear(); + state.load_screen_mode = 1; + state.current_screen = AppScreen::LOAD_QUIZ; + break; + case 9: + state.manual_topic = 0; + state.manual_scroll = 0; + state.current_screen = AppScreen::MANUAL; + break; + } + }; + auto option = MenuOption::Vertical(); option.entries_option.transform = [](EntryState es) { auto label = text(es.label) | center; @@ -30,10 +137,11 @@ ftxui::Component make_menu_screen(AppState& state) label = label | inverted; return label; }; + option.on_enter = activate_menu; auto menu = Menu(&entries, &state.menu_selected, option); auto component = Container::Vertical({menu}); - component |= CatchEvent([&](Event event) { + component |= CatchEvent([&, activate_menu](Event event) { if (event == Event::Character('0')) { state.manual_topic = 0; @@ -54,110 +162,8 @@ ftxui::Component make_menu_screen(AppState& state) if (event == Event::Return || (event.is_character() && event.character()[0] >= '1' && event.character()[0] <= '9')) { - switch (state.menu_selected) - { - case 0: - if (state.questions.empty()) - { - state.status_message = "No questions loaded."; - } - else if (state.loaded_files.size() <= 1) - { - state.start_quiz(); - } - else - { - state.quiz_setup_phase = 0; - state.quiz_setup_cursor = 0; - state.quiz_file_included.assign(state.loaded_files.size(), true); - state.quiz_file_order.clear(); - state.current_screen = AppScreen::QUIZ_SETUP; - } - return true; - case 1: - state.reset_add_form(); - state.current_screen = state.route_to(AppScreen::ADD_QUESTION); - return true; - case 2: - if (state.questions.empty()) - state.status_message = "No questions to remove."; - else - { - state.remove_question_idx = 0; - state.current_screen = state.route_to(AppScreen::REMOVE_QUESTION); - } - return true; - case 3: - if (state.questions.empty()) - state.status_message = "No questions available."; - else - { - state.select_question_idx = 0; - state.select_new_answer = 0; - state.change_answer_phase = 0; - state.current_screen = state.route_to(AppScreen::CHANGE_ANSWER); - } - return true; - case 4: - if (state.questions.empty()) - state.status_message = "No questions available."; - else - { - state.edit_choice_question_idx = 0; - state.edit_choice_choice_idx = 0; - state.edit_choice_phase = 0; - state.current_screen = state.route_to(AppScreen::EDIT_CHOICE); - } - return true; - case 5: - if (state.questions.empty()) - state.status_message = "No questions available."; - else - { - state.list_selected = 0; - state.current_screen = state.route_to(AppScreen::LIST_QUESTIONS); - } - return true; - case 6: - { - state.current_screen = state.route_to(AppScreen::SET_METADATA); - if (state.current_screen == AppScreen::SET_METADATA) - { - if (state.target_file >= 0 && state.target_file < static_cast(state.loaded_files.size())) - { - auto& lf = state.loaded_files[state.target_file]; - state.meta_name_text = lf.name; - state.meta_author_text = lf.author; - } - else - { - state.meta_name_text = state.quiz_name; - state.meta_author_text = state.quiz_author; - } - } - return true; - } - case 7: - if (state.loaded_files.empty()) - { - state.status_message = "No file loaded to save."; - return true; - } - state.current_screen = state.route_to(AppScreen::SAVE_CONFIRM); - if (state.current_screen == AppScreen::SAVE_CONFIRM) - state.compute_diff(state.target_file); - return true; - case 8: - state.load_path_text.clear(); - state.load_screen_mode = 1; - state.current_screen = AppScreen::LOAD_QUIZ; - return true; - case 9: - state.manual_topic = 0; - state.manual_scroll = 0; - state.current_screen = AppScreen::MANUAL; - return true; - } + activate_menu(); + return true; } return false; }); From 924ab92fc347ce232bdc48f2bfc3354d9cd56312 Mon Sep 17 00:00:00 2001 From: trintlermint Date: Sun, 5 Apr 2026 23:17:13 +0200 Subject: [PATCH 2/4] chore: pull out and abstract list entry further --- src/list_entry.hpp | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/list_entry.hpp diff --git a/src/list_entry.hpp b/src/list_entry.hpp new file mode 100644 index 0000000..5aebcae --- /dev/null +++ b/src/list_entry.hpp @@ -0,0 +1,36 @@ +#ifndef CERTAMEN_LIST_ENTRY_HPP +#define CERTAMEN_LIST_ENTRY_HPP + +#include +#include +#include +#include +#include + +using EntryBoxes = std::shared_ptr>; + +inline EntryBoxes make_entry_boxes() +{ + return std::make_shared>(); +} + +inline ftxui::Element list_entry(ftxui::Element el, bool selected, + EntryBoxes& boxes, int i) +{ + if (selected) el = el | ftxui::color(ftxui::Color::Cyan) | ftxui::focus; + return el | ftxui::reflect((*boxes)[i]); +} + +inline int mouse_click_index(ftxui::Event& event, const EntryBoxes& boxes) +{ + if (!event.is_mouse() || + event.mouse().button != ftxui::Mouse::Left || + event.mouse().motion != ftxui::Mouse::Pressed) + return -1; + for (int i = 0; i < static_cast(boxes->size()); ++i) + if ((*boxes)[i].Contain(event.mouse().x, event.mouse().y)) + return i; + return -1; +} + +#endif From 36d2b66d9d563ff8045e90370594eac8ff22f378 Mon Sep 17 00:00:00 2001 From: trintlermint Date: Sun, 5 Apr 2026 23:22:08 +0200 Subject: [PATCH 3/4] feat: use new abstract helper instead of writing duplicate code for mouse clicks and buttons --- src/screens/change_answer.cpp | 41 +++++++++++++++++++++++++++----- src/screens/edit_choice.cpp | 42 ++++++++++++++++++++++++++++----- src/screens/list_questions.cpp | 17 +++++++++---- src/screens/load_quiz.cpp | 33 +++++++++++++++++--------- src/screens/manual.cpp | 22 +++++++++++++---- src/screens/quiz.cpp | 31 +++++++++++++++++++++--- src/screens/quiz_setup.cpp | 20 ++++++++++++---- src/screens/remove_question.cpp | 18 ++++++++++---- 8 files changed, 180 insertions(+), 44 deletions(-) diff --git a/src/screens/change_answer.cpp b/src/screens/change_answer.cpp index f0ce579..0676bde 100644 --- a/src/screens/change_answer.cpp +++ b/src/screens/change_answer.cpp @@ -1,5 +1,6 @@ #include "screens/change_answer.hpp" #include "app.hpp" +#include "list_entry.hpp" #include "nav.hpp" #include "syntax.hpp" #include @@ -10,9 +11,32 @@ using namespace ftxui; ftxui::Component make_change_answer_screen(AppState& state) { - auto component = CatchEvent(Renderer([](bool) { return text(""); }), [&](Event event) { + auto entry_boxes = make_entry_boxes(); + + auto component = CatchEvent(Renderer([](bool) { return text(""); }), [&, entry_boxes](Event event) { if (state.target_indices.empty()) return false; + int clicked = mouse_click_index(event, entry_boxes); + if (clicked >= 0) + { + if (state.change_answer_phase == 0) + { + state.select_question_idx = clicked; + int real_idx = state.target_indices[clicked]; + state.change_answer_phase = 1; + state.select_new_answer = state.questions[real_idx].answer; + } + else + { + state.select_new_answer = clicked; + int real_idx = state.target_indices[state.select_question_idx]; + state.questions[real_idx].answer = state.select_new_answer; + state.status_message = "Answer updated."; + state.change_answer_phase = 0; + } + return true; + } + if (state.change_answer_phase == 0) { int count = static_cast(state.target_indices.size()); @@ -54,7 +78,7 @@ ftxui::Component make_change_answer_screen(AppState& state) return false; }); - return Renderer(component, [&] { + return Renderer(component, [&, entry_boxes] { if (state.target_indices.empty()) return text(" No questions for this file. ") | center | borderRounded; @@ -62,13 +86,16 @@ ftxui::Component make_change_answer_screen(AppState& state) if (state.change_answer_phase == 0) { + int count = static_cast(state.target_indices.size()); + entry_boxes->resize(count); + body.push_back(text("")); body.push_back(text(" Change Answer ") | bold | center); body.push_back(text("")); body.push_back(separator() | color(Color::GrayDark)); body.push_back(text("")); - for (int i = 0; i < static_cast(state.target_indices.size()); ++i) + for (int i = 0; i < count; ++i) { bool sel = (i == state.select_question_idx); const auto& q = state.questions[state.target_indices[i]]; @@ -82,7 +109,7 @@ ftxui::Component make_change_answer_screen(AppState& state) text(answer_hint) | dim | color(Color::Green), }); if (sel) entry = entry | color(Color::Cyan) | focus; - body.push_back(entry); + body.push_back(entry | reflect((*entry_boxes)[i])); } body.push_back(text("")); @@ -109,7 +136,9 @@ ftxui::Component make_change_answer_screen(AppState& state) body.push_back(text("")); - for (int i = 0; i < static_cast(q.choices.size()); ++i) + int num_choices = static_cast(q.choices.size()); + entry_boxes->resize(num_choices); + for (int i = 0; i < num_choices; ++i) { bool is_current = (i == q.answer); bool is_new = (i == state.select_new_answer); @@ -122,7 +151,7 @@ ftxui::Component make_change_answer_screen(AppState& state) }); if (is_new) choice_el = choice_el | color(Color::Cyan) | focus; else choice_el = choice_el | dim; - body.push_back(choice_el); + body.push_back(choice_el | reflect((*entry_boxes)[i])); } body.push_back(text("")); diff --git a/src/screens/edit_choice.cpp b/src/screens/edit_choice.cpp index 4b2c961..35905aa 100644 --- a/src/screens/edit_choice.cpp +++ b/src/screens/edit_choice.cpp @@ -1,5 +1,6 @@ #include "screens/edit_choice.hpp" #include "app.hpp" +#include "list_entry.hpp" #include "nav.hpp" #include "syntax.hpp" #include @@ -10,6 +11,7 @@ using namespace ftxui; ftxui::Component make_edit_choice_screen(AppState& state) { + auto entry_boxes = make_entry_boxes(); auto text_input = Input(&state.edit_choice_text, "Choice text..."); auto save_btn = Button(" Save ", [&] { @@ -35,8 +37,31 @@ ftxui::Component make_edit_choice_screen(AppState& state) auto inner = Container::Vertical({}); - auto component = CatchEvent(inner, [&](Event event) { + auto component = CatchEvent(inner, [&, entry_boxes](Event event) { if (state.target_indices.empty()) return false; + + if (state.edit_choice_phase != 2) + { + int clicked = mouse_click_index(event, entry_boxes); + if (clicked >= 0) + { + if (state.edit_choice_phase == 0) + { + state.edit_choice_question_idx = clicked; + state.edit_choice_phase = 1; + state.edit_choice_choice_idx = 0; + } + else + { + state.edit_choice_choice_idx = clicked; + int real_idx = state.target_indices[state.edit_choice_question_idx]; + state.edit_choice_text = state.questions[real_idx].choices[clicked]; + state.edit_choice_phase = 2; + } + return true; + } + } + if (state.edit_choice_phase == 2) { if (event == Event::Escape) @@ -87,7 +112,7 @@ ftxui::Component make_edit_choice_screen(AppState& state) }); return Renderer(component, [&, inner, focusable, edit_container, - text_input, save_btn, cancel_btn] { + text_input, save_btn, cancel_btn, entry_boxes] { inner->DetachAllChildren(); if (state.edit_choice_phase == 2) inner->Add(edit_container); @@ -101,13 +126,16 @@ ftxui::Component make_edit_choice_screen(AppState& state) if (state.edit_choice_phase == 0) { + int count = static_cast(state.target_indices.size()); + entry_boxes->resize(count); + body.push_back(text("")); body.push_back(text(" Edit Choice ") | bold | center); body.push_back(text("")); body.push_back(separator() | color(Color::GrayDark)); body.push_back(text("")); - for (int i = 0; i < static_cast(state.target_indices.size()); ++i) + for (int i = 0; i < count; ++i) { bool sel = (i == state.edit_choice_question_idx); const auto& q = state.questions[state.target_indices[i]]; @@ -117,7 +145,7 @@ ftxui::Component make_edit_choice_screen(AppState& state) text(q.question) | (sel ? bold : nothing), }); if (sel) entry = entry | color(Color::Cyan) | focus; - body.push_back(entry); + body.push_back(entry | reflect((*entry_boxes)[i])); } body.push_back(text("")); @@ -144,7 +172,9 @@ ftxui::Component make_edit_choice_screen(AppState& state) body.push_back(text("")); - for (int i = 0; i < static_cast(q.choices.size()); ++i) + int num_choices = static_cast(q.choices.size()); + entry_boxes->resize(num_choices); + for (int i = 0; i < num_choices; ++i) { bool sel = (i == state.edit_choice_choice_idx); bool is_answer = (i == q.answer); @@ -156,7 +186,7 @@ ftxui::Component make_edit_choice_screen(AppState& state) }); if (sel) choice_el = choice_el | color(Color::Cyan) | focus; else choice_el = choice_el | dim; - body.push_back(choice_el); + body.push_back(choice_el | reflect((*entry_boxes)[i])); } body.push_back(text("")); diff --git a/src/screens/list_questions.cpp b/src/screens/list_questions.cpp index 43a5a99..abab91c 100644 --- a/src/screens/list_questions.cpp +++ b/src/screens/list_questions.cpp @@ -1,5 +1,6 @@ #include "screens/list_questions.hpp" #include "app.hpp" +#include "list_entry.hpp" #include "nav.hpp" #include "syntax.hpp" #include @@ -15,11 +16,16 @@ ftxui::Component make_list_questions_screen(AppState& state) auto explain_toggle = Checkbox(" Explain", &state.list_show_explain); auto controls = Container::Horizontal({answer_toggle, code_toggle, explain_toggle}); + auto entry_boxes = make_entry_boxes(); auto component = Container::Vertical({controls}); - component |= CatchEvent([&](Event event) { + component |= CatchEvent([&, entry_boxes](Event event) { int count = static_cast(state.target_indices.size()); + + int clicked = mouse_click_index(event, entry_boxes); + if (clicked >= 0) { state.list_selected = clicked; return true; } + if (nav_up_down(event, state.list_selected, count)) return true; if (event == Event::Character('b') || event == Event::Escape) { @@ -29,9 +35,12 @@ ftxui::Component make_list_questions_screen(AppState& state) return false; }); - return Renderer(component, [&, answer_toggle, code_toggle, explain_toggle] { + return Renderer(component, [&, answer_toggle, code_toggle, explain_toggle, entry_boxes] { + int count = static_cast(state.target_indices.size()); + entry_boxes->resize(count); + Elements list_entries; - for (int i = 0; i < static_cast(state.target_indices.size()); ++i) + for (int i = 0; i < count; ++i) { bool selected = (i == state.list_selected); const auto& q = state.questions[state.target_indices[i]]; @@ -47,7 +56,7 @@ ftxui::Component make_list_questions_screen(AppState& state) : text(""), }); if (selected) entry = entry | color(Color::Cyan) | focus; - list_entries.push_back(entry); + list_entries.push_back(entry | reflect((*entry_boxes)[i])); } auto list_panel = vbox(std::move(list_entries)) diff --git a/src/screens/load_quiz.cpp b/src/screens/load_quiz.cpp index 5f4a9f8..9ea87be 100644 --- a/src/screens/load_quiz.cpp +++ b/src/screens/load_quiz.cpp @@ -1,5 +1,6 @@ #include "screens/load_quiz.hpp" #include "app.hpp" +#include "list_entry.hpp" #include "nav.hpp" #include #include @@ -28,16 +29,28 @@ ftxui::Component make_load_quiz_screen(AppState& state) auto action_row = Container::Horizontal({load_btn, done_btn}); auto input_area = Container::Vertical({path_input, action_row}); auto focusable = Renderer([](bool) { return text(""); }); + auto entry_boxes = make_entry_boxes(); - auto inner = Container::Vertical({}); + auto tab = Container::Tab({focusable, input_area}, &state.load_screen_mode); - auto component = CatchEvent(inner, [&](Event event) { + auto component = CatchEvent(tab, [&, entry_boxes](Event event) { if (event == Event::Escape) { state.return_to_menu(); return true; } + if (!state.loaded_files.empty()) + { + int clicked = mouse_click_index(event, entry_boxes); + if (clicked >= 0) + { + state.load_screen_mode = 0; + state.load_screen_selected = clicked; + return true; + } + } + if (event == Event::Tab) { if (!state.loaded_files.empty()) @@ -67,13 +80,9 @@ ftxui::Component make_load_quiz_screen(AppState& state) return false; }); - return Renderer(component, [&, inner, focusable, input_area, - path_input, load_btn, done_btn] { - inner->DetachAllChildren(); - if (state.load_screen_mode == 0 && !state.loaded_files.empty()) - inner->Add(focusable); - else - inner->Add(input_area); + return Renderer(component, [&, path_input, load_btn, done_btn, entry_boxes] { + if (state.loaded_files.empty()) + state.load_screen_mode = 1; Elements body; body.push_back(text("")); @@ -87,7 +96,9 @@ ftxui::Component make_load_quiz_screen(AppState& state) body.push_back(text(" Loaded files:") | dim); body.push_back(text("")); - for (int i = 0; i < static_cast(state.loaded_files.size()); ++i) + int file_count = static_cast(state.loaded_files.size()); + entry_boxes->resize(file_count); + for (int i = 0; i < file_count; ++i) { bool sel = (state.load_screen_mode == 0 && i == state.load_screen_selected); const auto& lf = state.loaded_files[i]; @@ -104,7 +115,7 @@ ftxui::Component make_load_quiz_screen(AppState& state) sel ? (text(" unload →") | color(Color::RedLight)) : text(""), }); if (sel) entry = entry | color(Color::Cyan); - body.push_back(entry); + body.push_back(entry | reflect((*entry_boxes)[i])); } body.push_back(text("")); diff --git a/src/screens/manual.cpp b/src/screens/manual.cpp index 2fefd6c..1343922 100644 --- a/src/screens/manual.cpp +++ b/src/screens/manual.cpp @@ -1,5 +1,6 @@ #include "screens/manual.hpp" #include "app.hpp" +#include "list_entry.hpp" #include "nav.hpp" #include #include @@ -209,11 +210,20 @@ static const std::vector& get_sections() ftxui::Component make_manual_screen(AppState& state) { auto focusable = Renderer([](bool) { return text(""); }); + auto entry_boxes = make_entry_boxes(); - auto component = CatchEvent(focusable, [&](Event event) { + auto component = CatchEvent(focusable, [&, entry_boxes](Event event) { const auto& sections = get_sections(); int topic_count = static_cast(sections.size()); + int clicked = mouse_click_index(event, entry_boxes); + if (clicked >= 0) + { + state.manual_topic = clicked; + state.manual_scroll = 0; + return true; + } + if (nav_up_down(event, state.manual_topic, topic_count)) { state.manual_scroll = 0; @@ -231,20 +241,22 @@ ftxui::Component make_manual_screen(AppState& state) return false; }); - return Renderer(component, [&] { + return Renderer(component, [&, entry_boxes] { const auto& sections = get_sections(); // left panel: topic list + int topic_count = static_cast(sections.size()); + entry_boxes->resize(topic_count); Elements toc_entries; - for (int i = 0; i < static_cast(sections.size()); ++i) + for (int i = 0; i < topic_count; ++i) { bool sel = (i == state.manual_topic); auto entry = hbox({ text(sel ? " > " : " "), text(sections[i].title) | (sel ? bold : nothing), }); - if (sel) entry = entry | color(Color::Cyan); - toc_entries.push_back(entry); + if (sel) entry = entry | color(Color::Cyan) | focus; + toc_entries.push_back(entry | reflect((*entry_boxes)[i])); } auto toc_panel = vbox({ diff --git a/src/screens/quiz.cpp b/src/screens/quiz.cpp index cdf75f4..e663d8e 100644 --- a/src/screens/quiz.cpp +++ b/src/screens/quiz.cpp @@ -1,5 +1,6 @@ #include "screens/quiz.hpp" #include "app.hpp" +#include "list_entry.hpp" #include "nav.hpp" #include "syntax.hpp" #include @@ -22,8 +23,9 @@ static std::string format_pct(int correct, int total) ftxui::Component make_quiz_screen(AppState& state) { auto focusable = Renderer([](bool) { return text(""); }); + auto choice_boxes = make_entry_boxes(); - auto component = CatchEvent(focusable, [&](Event event) { + auto component = CatchEvent(focusable, [&, choice_boxes](Event event) { if (state.quiz_session.empty()) return false; if (state.quiz_quit_pending) @@ -48,6 +50,28 @@ ftxui::Component make_quiz_screen(AppState& state) const auto& q = state.quiz_session[idx]; int num_choices = static_cast(q.choices.size()); + int clicked = mouse_click_index(event, choice_boxes); + if (clicked >= 0) + { + if (!state.quiz_answered) + { + state.quiz_selected = clicked; + state.quiz_answered = true; + state.quiz_was_correct = (clicked == q.answer); + if (state.quiz_was_correct) ++state.quiz_score; + } + else + { + state.quiz_index++; + state.quiz_selected = 0; + state.quiz_answered = false; + state.quiz_was_correct = false; + if (state.quiz_index >= static_cast(state.quiz_session.size())) + state.current_screen = AppScreen::QUIZ_RESULT; + } + return true; + } + if (!state.quiz_answered) { if (nav_up_down(event, state.quiz_selected, num_choices)) return true; @@ -83,7 +107,7 @@ ftxui::Component make_quiz_screen(AppState& state) return false; }); - return Renderer(component, [&] { + return Renderer(component, [&, choice_boxes] { if (state.quiz_quit_pending) { return vbox({ @@ -157,6 +181,7 @@ ftxui::Component make_quiz_screen(AppState& state) body.push_back(text("")); + choice_boxes->resize(q.choices.size()); for (int i = 0; i < static_cast(q.choices.size()); ++i) { bool selected = (i == state.quiz_selected); @@ -189,7 +214,7 @@ ftxui::Component make_quiz_screen(AppState& state) else choice_el = choice_el | dim; - body.push_back(choice_el); + body.push_back(choice_el | reflect((*choice_boxes)[i])); } if (state.quiz_answered) diff --git a/src/screens/quiz_setup.cpp b/src/screens/quiz_setup.cpp index c2c491f..0f5d834 100644 --- a/src/screens/quiz_setup.cpp +++ b/src/screens/quiz_setup.cpp @@ -1,5 +1,6 @@ #include "screens/quiz_setup.hpp" #include "app.hpp" +#include "list_entry.hpp" #include "nav.hpp" #include #include @@ -10,8 +11,12 @@ using namespace ftxui; ftxui::Component make_quiz_setup_screen(AppState& state) { auto focusable = Renderer([](bool) { return text(""); }); + auto entry_boxes = make_entry_boxes(); + + auto component = CatchEvent(focusable, [&, entry_boxes](Event event) { + int clicked = mouse_click_index(event, entry_boxes); + if (clicked >= 0) { state.quiz_setup_cursor = clicked; return true; } - auto component = CatchEvent(focusable, [&](Event event) { if (event == Event::Escape) { state.return_to_menu(); @@ -118,7 +123,7 @@ ftxui::Component make_quiz_setup_screen(AppState& state) return false; }); - return Renderer(component, [&] { + return Renderer(component, [&, entry_boxes] { int file_count = static_cast(state.loaded_files.size()); Elements body; @@ -126,6 +131,8 @@ ftxui::Component make_quiz_setup_screen(AppState& state) if (state.quiz_setup_phase == 0) { + entry_boxes->resize(file_count); + body.push_back(text(" Select Quiz Files ") | bold | center); body.push_back(text("")); body.push_back(separator() | color(Color::GrayDark)); @@ -150,7 +157,7 @@ ftxui::Component make_quiz_setup_screen(AppState& state) }); if (sel) entry = entry | color(Color::Cyan); if (!included) entry = entry | dim; - body.push_back(entry); + body.push_back(entry | reflect((*entry_boxes)[i])); } body.push_back(text("")); @@ -160,12 +167,15 @@ ftxui::Component make_quiz_setup_screen(AppState& state) } else { + int order_count = static_cast(state.quiz_file_order.size()); + entry_boxes->resize(order_count); + body.push_back(text(" Set Quiz Order ") | bold | center); body.push_back(text("")); body.push_back(separator() | color(Color::GrayDark)); body.push_back(text("")); - for (int pos = 0; pos < static_cast(state.quiz_file_order.size()); ++pos) + for (int pos = 0; pos < order_count; ++pos) { bool sel = (pos == state.quiz_setup_cursor); int fi = state.quiz_file_order[pos]; @@ -182,7 +192,7 @@ ftxui::Component make_quiz_setup_screen(AppState& state) text(" " + std::to_string(qcount) + "q") | dim, }); if (sel) entry = entry | color(Color::Cyan); - body.push_back(entry); + body.push_back(entry | reflect((*entry_boxes)[pos])); } body.push_back(text("")); diff --git a/src/screens/remove_question.cpp b/src/screens/remove_question.cpp index aca51ea..50b230b 100644 --- a/src/screens/remove_question.cpp +++ b/src/screens/remove_question.cpp @@ -1,5 +1,6 @@ #include "screens/remove_question.hpp" #include "app.hpp" +#include "list_entry.hpp" #include "nav.hpp" #include #include @@ -9,9 +10,15 @@ using namespace ftxui; ftxui::Component make_remove_question_screen(AppState& state) { - auto component = CatchEvent(Renderer([](bool) { return text(""); }), [&](Event event) { + auto entry_boxes = make_entry_boxes(); + + auto component = CatchEvent(Renderer([](bool) { return text(""); }), [&, entry_boxes](Event event) { if (state.target_indices.empty()) return false; int count = static_cast(state.target_indices.size()); + + int clicked = mouse_click_index(event, entry_boxes); + if (clicked >= 0) { state.remove_question_idx = clicked; return true; } + if (nav_up_down(event, state.remove_question_idx, count)) return true; if (nav_numeric(event, state.remove_question_idx, count)) return true; if (event == Event::Return) @@ -37,10 +44,13 @@ ftxui::Component make_remove_question_screen(AppState& state) return false; }); - return Renderer(component, [&] { + return Renderer(component, [&, entry_boxes] { if (state.target_indices.empty()) return text(" No questions for this file. ") | center | borderRounded; + int count = static_cast(state.target_indices.size()); + entry_boxes->resize(count); + Elements body; body.push_back(text("")); body.push_back(text(" Remove Question ") | bold | center); @@ -48,7 +58,7 @@ ftxui::Component make_remove_question_screen(AppState& state) body.push_back(separator() | color(Color::GrayDark)); body.push_back(text("")); - for (int i = 0; i < static_cast(state.target_indices.size()); ++i) + for (int i = 0; i < count; ++i) { bool sel = (i == state.remove_question_idx); const auto& q = state.questions[state.target_indices[i]]; @@ -64,7 +74,7 @@ ftxui::Component make_remove_question_screen(AppState& state) : text(""), }); if (sel) entry = entry | color(Color::Cyan) | focus; - body.push_back(entry); + body.push_back(entry | reflect((*entry_boxes)[i])); } body.push_back(text("")); From 3f55cb865644dfd0eb714635e30e290542923986 Mon Sep 17 00:00:00 2001 From: trintlermint Date: Sun, 5 Apr 2026 23:22:46 +0200 Subject: [PATCH 4/4] feat: add mouse buttons on main menu, this finishes and fixes #15 --- src/screens/menu.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/screens/menu.cpp b/src/screens/menu.cpp index 8744193..dad0bab 100644 --- a/src/screens/menu.cpp +++ b/src/screens/menu.cpp @@ -128,20 +128,35 @@ ftxui::Component make_menu_screen(AppState& state) } }; + auto menu_box = std::make_shared>(entries.size()); auto option = MenuOption::Vertical(); - option.entries_option.transform = [](EntryState es) { + option.entries_option.transform = [menu_box](EntryState es) { auto label = text(es.label) | center; if (es.focused) label = label | bold; if (es.active) label = label | inverted; + if (es.index < static_cast(menu_box->size())) + label = label | reflect((*menu_box)[es.index]); return label; }; option.on_enter = activate_menu; auto menu = Menu(&entries, &state.menu_selected, option); auto component = Container::Vertical({menu}); - component |= CatchEvent([&, activate_menu](Event event) { + component |= CatchEvent([&, activate_menu, menu_box](Event event) { + if (event.is_mouse() && event.mouse().button == Mouse::Left && + event.mouse().motion == Mouse::Pressed) + { + for (int i = 0; i < static_cast(menu_box->size()); ++i) + if ((*menu_box)[i].Contain(event.mouse().x, event.mouse().y)) + { + state.menu_selected = i; + activate_menu(); + return true; + } + } + if (event == Event::Character('0')) { state.manual_topic = 0;