From e8d615985b3428e002446624fc1c590d71eea0a6 Mon Sep 17 00:00:00 2001 From: hz Date: Wed, 22 Jul 2026 17:21:23 -0500 Subject: [PATCH] Reveal the flashcard answer by tapping the card, not a separate button Matches the mockup: the "Show Answer" button is replaced by an in-card "Tap card to reveal answer" hint, and clicking anywhere on the card reveals it (guarded the same way Space already was - only while at the live card and not yet revealed). --- scripts/ui/flashcards_study.gd | 45 ++++++++++++---------- tests/flashcard_session_navigation_test.gd | 4 +- tests/flashcard_study_render_test.gd | 21 +++++++++- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/scripts/ui/flashcards_study.gd b/scripts/ui/flashcards_study.gd index 74601ef..c135cb7 100644 --- a/scripts/ui/flashcards_study.gd +++ b/scripts/ui/flashcards_study.gd @@ -55,7 +55,7 @@ var back_label: Label var extra_caption: Label var extra_label: Label var card_position_label: Label -var reveal_button: Button +var tap_hint_label: Label var grade_row: HBoxContainer var grade_buttons: Dictionary = {} var undo_button: Button @@ -184,6 +184,8 @@ func _build() -> void: card_panel = PanelContainer.new() card_panel.custom_minimum_size = Vector2(720, 420) card_panel.add_theme_stylebox_override("panel", Brand.content_card_box(UIThemeService.is_dark_mode(), true, Brand.ROYAL_BLUE)) + card_panel.mouse_filter = Control.MOUSE_FILTER_STOP + card_panel.gui_input.connect(_on_card_panel_input) center.add_child(card_panel) var card_content := VBoxContainer.new() card_content.add_theme_constant_override("separation", 20) @@ -206,6 +208,13 @@ func _build() -> void: divider.visible = false card_content.add_child(divider) + var tap_hint_center := CenterContainer.new() + card_content.add_child(tap_hint_center) + tap_hint_label = Label.new() + tap_hint_label.text = "Tap card to reveal answer" + tap_hint_label.visible = false + tap_hint_center.add_child(tap_hint_label) + var answer_block := VBoxContainer.new() answer_block.add_theme_constant_override("separation", 4) card_content.add_child(answer_block) @@ -240,13 +249,6 @@ func _build() -> void: card_position_label.visible = false position_row.add_child(card_position_label) - reveal_button = Button.new() - reveal_button.text = "Show Answer (Space)" - reveal_button.custom_minimum_size = Vector2(0, 52) - _style_gradient_button(reveal_button) - reveal_button.pressed.connect(_reveal) - center_column.add_child(reveal_button) - grade_row = HBoxContainer.new() grade_row.add_theme_constant_override("separation", 10) grade_row.visible = false @@ -309,14 +311,6 @@ func _caption_label(text: String) -> Label: return label -func _style_gradient_button(button: Button) -> void: - button.add_theme_stylebox_override("normal", Brand.gradient_button_box()) - button.add_theme_stylebox_override("hover", Brand.gradient_button_box()) - button.add_theme_stylebox_override("pressed", Brand.gradient_button_box()) - button.add_theme_color_override("font_color", Color.WHITE) - button.add_theme_color_override("font_hover_color", Color.WHITE) - - func _style_ghost_button(button: Button, dark: bool) -> void: var empty := StyleBoxEmpty.new() button.add_theme_stylebox_override("normal", empty) @@ -565,6 +559,7 @@ func refresh_theme_controls() -> void: extra_caption.add_theme_color_override("font_color", Brand.muted_color(dark)) extra_label.add_theme_color_override("font_color", Brand.muted_color(dark)) card_position_label.add_theme_color_override("font_color", Brand.muted_color(dark)) + tap_hint_label.add_theme_color_override("font_color", Brand.muted_color(dark)) _style_ghost_button(undo_button, dark) previous_button.add_theme_stylebox_override("normal", Brand.secondary_button_box(dark)) previous_button.add_theme_stylebox_override("hover", Brand.secondary_button_box(dark, true)) @@ -706,7 +701,7 @@ func _render_history_entry(index: int) -> void: _render_card() _reveal() grade_row.visible = false - reveal_button.visible = false + tap_hint_label.visible = false var is_graded_entry: bool = entry.get("action", "") == "graded" var is_last_graded_card: bool = str(entry.get("card_id", "")) == last_card_id var has_undoable_review: bool = not last_client_review_id.is_empty() @@ -811,7 +806,7 @@ func _render_loading() -> void: front_label.visible = true front_label.text = "Loading..." _hide_answer() - reveal_button.visible = false + tap_hint_label.visible = false grade_row.visible = false @@ -819,7 +814,7 @@ func _render_empty(card: Dictionary) -> void: empty_label.visible = true front_label.visible = false _hide_answer() - reveal_button.visible = false + tap_hint_label.visible = false grade_row.visible = false progress_label.text = "0 due" if typeof(card) == TYPE_DICTIONARY: @@ -844,7 +839,8 @@ func _render_card() -> void: front_label.text = str(current_note.get("prompt", "")) front_label.visible = true _hide_answer() - reveal_button.visible = true + divider.visible = true + tap_hint_label.visible = true grade_row.visible = false progress_label.text = "%d due, %d new available" % [due_remaining, new_remaining] @@ -866,13 +862,20 @@ func _reveal() -> void: extra_caption.visible = not extra.is_empty() extra_label.visible = not extra.is_empty() extra_label.text = extra - reveal_button.visible = false + tap_hint_label.visible = false grade_row.visible = true for rating in grade_buttons: var button: Button = grade_buttons[rating] button.text = "%s\n%s" % [rating.capitalize(), _interval_label_for(rating)] +func _on_card_panel_input(event: InputEvent) -> void: + if history_cursor != session_history.size() or revealed: + return + if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + _reveal() + + func _interval_label_for(rating: String) -> String: for option in rating_options: if str(option.get("rating", "")) == rating: diff --git a/tests/flashcard_session_navigation_test.gd b/tests/flashcard_session_navigation_test.gd index 3f77290..b6dc807 100644 --- a/tests/flashcard_session_navigation_test.gd +++ b/tests/flashcard_session_navigation_test.gd @@ -135,8 +135,8 @@ func _run() -> void: screen._go_next() _assert(screen.front_label.text == "P3(live)" and screen.history_cursor == screen.session_history.size(), "stepping past the last history entry should return to the live card") - _assert(screen.reveal_button.visible and not screen.grade_row.visible, - "returning to a live, unrevealed card should restore the reveal prompt") + _assert(screen.tap_hint_label.visible and not screen.grade_row.visible, + "returning to a live, unrevealed card should restore the tap-to-reveal hint") screen._go_next() _assert(screen.session_history.size() == 3 and screen.session_history[2]["action"] == "skipped" and screen.session_history[2]["card_id"] == "card-3", diff --git a/tests/flashcard_study_render_test.gd b/tests/flashcard_study_render_test.gd index c574886..76f4751 100644 --- a/tests/flashcard_study_render_test.gd +++ b/tests/flashcard_study_render_test.gd @@ -29,7 +29,7 @@ func _initialize() -> void: ] screen._render_card() _assert(screen.front_label.text == "2 + 2 = ?", "the front should show only the prompt before reveal") - _assert(screen.reveal_button.visible, "the reveal button should be visible before the answer is shown") + _assert(screen.tap_hint_label.visible, "the tap-to-reveal hint should be visible before the answer is shown") _assert(not screen.grade_row.visible, "grading buttons should be hidden before reveal") _assert(not screen.back_label.visible, "the back should stay hidden before reveal") @@ -38,7 +38,7 @@ func _initialize() -> void: _assert(screen.extra_label.text == "Basic arithmetic" and screen.extra_label.visible, "the extra takeaway should render in its own labeled section") _assert(screen.answer_caption.visible and screen.extra_caption.visible, "revealing a card with an extra should show both the answer and takeaway captions") _assert(screen.grade_row.visible, "grading buttons should appear after reveal") - _assert(not screen.reveal_button.visible, "the reveal button should hide once the card is revealed") + _assert(not screen.tap_hint_label.visible, "the tap-to-reveal hint should hide once the card is revealed") _assert(screen.grade_buttons["good"].text.contains("1d"), "each grade button should show its computed next interval") # Cloze card: only the card's own variant is hidden on the front; back reveals everything. @@ -60,6 +60,23 @@ func _initialize() -> void: screen._reveal() _assert(not screen.extra_label.visible and not screen.extra_caption.visible, "a card with no extra takeaway should hide that section instead of showing an empty caption") + # Tapping the card panel reveals the answer, same as pressing Space. + screen.current_note = {"type": "basic", "prompt": "Tap test", "answer": "Tapped", "extra": ""} + screen.current_card = {"id": "card-4", "variant": 0, "revision": 1} + screen.revealed = false + screen._render_card() + var click := InputEventMouseButton.new() + click.button_index = MOUSE_BUTTON_LEFT + click.pressed = true + screen._on_card_panel_input(click) + _assert(screen.revealed and screen.back_label.text == "Tapped", "clicking the card panel should reveal the answer") + + # A second tap after reveal (or while browsing history) is a no-op - it must not re-trigger + # any reveal side effects (grading is guarded elsewhere; this only checks idempotence). + screen.back_label.text = "Tapped" + screen._on_card_panel_input(click) + _assert(screen.back_label.text == "Tapped", "tapping an already-revealed card should be a no-op") + screen.free() quit(1 if failures > 0 else 0)