From ee5cc29577e1788410fd10b4935bab1b3ea60d34 Mon Sep 17 00:00:00 2001 From: Richard Qin Date: Mon, 13 Apr 2026 00:56:38 +0800 Subject: feat: add support for interactive checkbox and radio button input fields --- include/ricochet/render/renderer.hh | 1 + src/core/browser.cc | 54 ++++++++++++++++++++++++++++++++++++- src/render/renderer.cc | 53 +++++++++++++++++++++++------------- 3 files changed, 88 insertions(+), 20 deletions(-) diff --git a/include/ricochet/render/renderer.hh b/include/ricochet/render/renderer.hh index dda3ac5..472afaa 100644 --- a/include/ricochet/render/renderer.hh +++ b/include/ricochet/render/renderer.hh @@ -30,6 +30,7 @@ struct InputField std::string method; std::string value {}; std::string type {}; + bool checked { false }; }; struct RenderResult { diff --git a/src/core/browser.cc b/src/core/browser.cc index e6c63d4..d463b73 100644 --- a/src/core/browser.cc +++ b/src/core/browser.cc @@ -379,6 +379,9 @@ InputAction submit_form( const render::InputField& target_input, std::string body; for ( const auto& in : inputs ) { if ( in.action == target_input.action && !in.name.empty() && in.type != "submit" && in.type != "button" ) { + if ( ( in.type == "checkbox" || in.type == "radio" ) && !in.checked ) { + continue; + } std::string encoded = in.value; for ( char& ch : encoded ) { if ( ch == ' ' ) { @@ -422,6 +425,40 @@ void update_input_value( PageData& page_data, std::size_t index, const std::stri } } +void update_single_line_ui( std::string& line, const std::string& marker, const render::InputField& in ) +{ + const std::size_t pos = line.find( marker ); + if ( pos == std::string::npos ) { + return; + } + const std::size_t end = line.find( ']', pos ); + if ( end == std::string::npos ) { + return; + } + + std::string prefix; + if ( in.type == "checkbox" ) { + prefix = in.checked ? "[X] " : "[ ] "; + } else { + prefix = in.checked ? "(X) " : "( ) "; + } + line.replace( pos, end - pos + 1, marker + prefix + in.value + "]" ); +} + +void update_toggle_ui( PageData& page_data ) +{ + for ( std::size_t i = 0; i < page_data.inputs.size(); ++i ) { + const auto& in = page_data.inputs[i]; + if ( in.type != "checkbox" && in.type != "radio" ) { + continue; + } + const std::string marker = "[I" + std::to_string( i + 1 ) + ":"; + for ( auto& line : page_data.lines ) { + update_single_line_ui( line, marker, in ); + } + } +} + InputAction handle_form_input( const tui::Terminal& terminal, PageData& page_data, const std::string& current_url, @@ -442,11 +479,26 @@ InputAction handle_form_input( const tui::Terminal& terminal, return InputAction::None; } - const auto& target_input = page_data.inputs[index - 1]; + auto& target_input = page_data.inputs[index - 1]; if ( target_input.type == "submit" || target_input.type == "button" ) { return submit_form( target_input, page_data.inputs, current_url, history, history_idx ); } + if ( target_input.type == "checkbox" || target_input.type == "radio" ) { + if ( target_input.type == "radio" ) { + for ( auto& in : page_data.inputs ) { + if ( in.name == target_input.name && in.action == target_input.action ) { + in.checked = false; + } + } + target_input.checked = true; + } else { + target_input.checked = !target_input.checked; + } + update_toggle_ui( page_data ); + return InputAction::None; + } + const std::string query = get_terminal_input( terminal, "Enter Text [I" + std::to_string( index ) + "]:" ); if ( query.empty() ) { return InputAction::None; diff --git a/src/render/renderer.cc b/src/render/renderer.cc index 4b152b7..3744728 100644 --- a/src/render/renderer.cc +++ b/src/render/renderer.cc @@ -71,25 +71,35 @@ std::string decode_entities( std::string text ) std::string extract_attr( const std::string& tag_name, std::string_view attr_name ) { - std::string match = std::string( attr_name ) + "=\""; + std::string match = " " + std::string( attr_name ) + "="; std::size_t pos = tag_name.find( match ); - if ( pos != std::string::npos ) { - pos += match.length(); - const std::size_t end = tag_name.find( '"', pos ); - if ( end != std::string::npos ) { - return tag_name.substr( pos, end - pos ); + + if ( pos == std::string::npos ) { + match = std::string( attr_name ) + "="; + if ( !tag_name.starts_with( match ) ) { + return ""; } + pos = 0; + } else { + pos += 1; + } + + pos += attr_name.length() + 1; + if ( pos >= tag_name.length() ) { + return ""; } - match = std::string( attr_name ) + "='"; - pos = tag_name.find( match ); - if ( pos != std::string::npos ) { - pos += match.length(); - const std::size_t end = tag_name.find( '\'', pos ); + + const char quote = tag_name[pos]; + if ( quote == '"' || quote == '\'' ) { + const std::size_t end = tag_name.find( quote, pos + 1 ); if ( end != std::string::npos ) { - return tag_name.substr( pos, end - pos ); + return tag_name.substr( pos + 1, end - pos - 1 ); } + return ""; } - return ""; + + const std::size_t end = tag_name.find_first_of( " >/", pos ); + return tag_name.substr( pos, ( end == std::string::npos ) ? std::string::npos : end - pos ); } void render_link_end( const parser::DomNode& node, RenderResult& result ) @@ -114,10 +124,10 @@ void render_img( const parser::DomNode& node, RenderResult& result ) } } -std::string build_input_hint( const std::string& tag_name ) +std::string build_input_hint( const std::string& tag_name, bool checked ) { std::string placeholder = extract_attr( tag_name, "placeholder" ); - std::string type = extract_attr( tag_name, "type" ); + const std::string type = extract_attr( tag_name, "type" ); std::string val = extract_attr( tag_name, "value" ); if ( !placeholder.empty() ) { @@ -126,8 +136,11 @@ std::string build_input_hint( const std::string& tag_name ) if ( type == "submit" || type == "button" ) { return val.empty() ? "Submit" : val; } - if ( type == "checkbox" || type == "radio" ) { - return type; + if ( type == "checkbox" ) { + return checked ? "[X] " + val : "[ ] " + val; + } + if ( type == "radio" ) { + return checked ? "(X) " + val : "( ) " + val; } if ( type == "hidden" ) { return "hidden"; @@ -167,9 +180,11 @@ void render_input_node( const parser::DomNode& node, type = "submit"; } const std::string default_val = extract_attr( node.tag_name, "value" ); - result.inputs.push_back( { extract_attr( node.tag_name, "name" ), ctx.action, ctx.method, default_val, type } ); + const bool is_checked = node.tag_name.contains( "checked" ); + result.inputs.push_back( + { extract_attr( node.tag_name, "name" ), ctx.action, ctx.method, default_val, type, is_checked } ); - std::string hint = build_input_hint( node.tag_name ); + std::string hint = build_input_hint( node.tag_name, is_checked ); if ( base_tag == "button" ) { hint = "Submit"; } -- cgit v1.2.3