⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/browser/
forms.rs

1//! HTML Form Elements and Scroll State
2//!
3//! Handles interactive form inputs (text fields, checkboxes, buttons),
4//! link navigation, and scroll state management. All dimensions use
5//! pixel coordinates (i32).
6
7#![allow(dead_code)]
8
9use alloc::{
10    string::{String, ToString},
11    vec::Vec,
12};
13
14use super::events::NodeId;
15
16// ---------------------------------------------------------------------------
17// Input types and form elements
18// ---------------------------------------------------------------------------
19
20/// HTML input element types
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum InputType {
23    #[default]
24    Text,
25    Password,
26    Submit,
27    Hidden,
28    Checkbox,
29    Radio,
30    Button,
31    Reset,
32}
33
34/// HTTP method for form submission
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum FormMethod {
37    #[default]
38    Get,
39    Post,
40}
41
42/// A <form> element's state
43#[derive(Debug, Clone, Default)]
44pub struct FormElement {
45    /// Form node ID in the DOM
46    pub node_id: NodeId,
47    /// Action URL
48    pub action: String,
49    /// Submission method
50    pub method: FormMethod,
51    /// Input element node IDs belonging to this form
52    pub inputs: Vec<NodeId>,
53}
54
55impl FormElement {
56    pub fn new(node_id: NodeId, action: &str, method: FormMethod) -> Self {
57        Self {
58            node_id,
59            action: action.to_string(),
60            method,
61            inputs: Vec::new(),
62        }
63    }
64
65    /// Add an input element to this form
66    pub fn add_input(&mut self, input_node: NodeId) {
67        self.inputs.push(input_node);
68    }
69
70    /// Build URL-encoded form data from input elements
71    pub fn encode_form_data(&self, inputs: &[InputElement]) -> String {
72        let mut result = String::new();
73        for input in inputs {
74            if input.input_type == InputType::Submit
75                || input.input_type == InputType::Button
76                || input.input_type == InputType::Reset
77            {
78                continue;
79            }
80            if input.input_type == InputType::Checkbox && !input.checked {
81                continue;
82            }
83            if !result.is_empty() {
84                result.push('&');
85            }
86            result.push_str(&url_encode(&input.name));
87            result.push('=');
88            result.push_str(&url_encode(&input.value));
89        }
90        result
91    }
92}
93
94/// An <input> element's state
95#[derive(Debug, Clone, Default)]
96pub struct InputElement {
97    /// Node ID in the DOM
98    pub node_id: NodeId,
99    /// Input type
100    pub input_type: InputType,
101    /// Name attribute
102    pub name: String,
103    /// Current value
104    pub value: String,
105    /// Whether the input is checked (checkbox/radio)
106    pub checked: bool,
107    /// Cursor position within value (for text inputs)
108    pub cursor_pos: usize,
109    /// Whether the input has focus
110    pub focused: bool,
111    /// Placeholder text
112    pub placeholder: String,
113    /// Whether the input is disabled
114    pub disabled: bool,
115    /// Maximum length (0 = unlimited)
116    pub max_length: usize,
117}
118
119impl InputElement {
120    pub fn new(node_id: NodeId, input_type: InputType, name: &str) -> Self {
121        Self {
122            node_id,
123            input_type,
124            name: name.to_string(),
125            value: String::new(),
126            checked: false,
127            cursor_pos: 0,
128            focused: false,
129            placeholder: String::new(),
130            disabled: false,
131            max_length: 0,
132        }
133    }
134
135    /// Set the value and clamp cursor
136    pub fn set_value(&mut self, value: &str) {
137        self.value = value.to_string();
138        if self.cursor_pos > self.value.len() {
139            self.cursor_pos = self.value.len();
140        }
141    }
142
143    /// Toggle checked state (for checkbox/radio)
144    pub fn toggle_checked(&mut self) {
145        if self.input_type == InputType::Checkbox || self.input_type == InputType::Radio {
146            self.checked = !self.checked;
147        }
148    }
149
150    /// Get display text (masked for password fields)
151    pub fn display_text(&self) -> String {
152        if self.value.is_empty() && !self.placeholder.is_empty() {
153            return self.placeholder.clone();
154        }
155        match self.input_type {
156            InputType::Password => {
157                let mut masked = String::with_capacity(self.value.len());
158                for _ in 0..self.value.len() {
159                    masked.push('*');
160                }
161                masked
162            }
163            _ => self.value.clone(),
164        }
165    }
166}
167
168// ---------------------------------------------------------------------------
169// Text input handling
170// ---------------------------------------------------------------------------
171
172/// Text input buffer with cursor and selection
173#[derive(Debug, Clone, Default)]
174pub struct TextInput {
175    /// Text buffer
176    pub buffer: String,
177    /// Cursor position (byte offset)
178    pub cursor: usize,
179    /// Selection start (byte offset, None if no selection)
180    pub selection_start: Option<usize>,
181    /// Selection end (byte offset)
182    pub selection_end: Option<usize>,
183}
184
185impl TextInput {
186    pub fn new() -> Self {
187        Self::default()
188    }
189
190    pub fn from_text(text: &str) -> Self {
191        Self {
192            buffer: text.to_string(),
193            cursor: text.len(),
194            selection_start: None,
195            selection_end: None,
196        }
197    }
198
199    /// Insert a character at the cursor position
200    pub fn insert_char(&mut self, ch: char) {
201        self.delete_selection();
202        if self.cursor > self.buffer.len() {
203            self.cursor = self.buffer.len();
204        }
205        self.buffer.insert(self.cursor, ch);
206        self.cursor += ch.len_utf8();
207    }
208
209    /// Insert a string at the cursor position
210    pub fn insert_str(&mut self, s: &str) {
211        self.delete_selection();
212        if self.cursor > self.buffer.len() {
213            self.cursor = self.buffer.len();
214        }
215        self.buffer.insert_str(self.cursor, s);
216        self.cursor += s.len();
217    }
218
219    /// Delete character before cursor (backspace)
220    pub fn backspace(&mut self) -> bool {
221        if self.delete_selection() {
222            return true;
223        }
224        if self.cursor == 0 {
225            return false;
226        }
227        // Find the previous character boundary
228        let prev = self.prev_char_boundary(self.cursor);
229        self.buffer.drain(prev..self.cursor);
230        self.cursor = prev;
231        true
232    }
233
234    /// Delete character at cursor (delete key)
235    pub fn delete(&mut self) -> bool {
236        if self.delete_selection() {
237            return true;
238        }
239        if self.cursor >= self.buffer.len() {
240            return false;
241        }
242        let next = self.next_char_boundary(self.cursor);
243        self.buffer.drain(self.cursor..next);
244        true
245    }
246
247    /// Move cursor left
248    pub fn move_left(&mut self) {
249        self.clear_selection();
250        if self.cursor > 0 {
251            self.cursor = self.prev_char_boundary(self.cursor);
252        }
253    }
254
255    /// Move cursor right
256    pub fn move_right(&mut self) {
257        self.clear_selection();
258        if self.cursor < self.buffer.len() {
259            self.cursor = self.next_char_boundary(self.cursor);
260        }
261    }
262
263    /// Move cursor to beginning
264    pub fn move_home(&mut self) {
265        self.clear_selection();
266        self.cursor = 0;
267    }
268
269    /// Move cursor to end
270    pub fn move_end(&mut self) {
271        self.clear_selection();
272        self.cursor = self.buffer.len();
273    }
274
275    /// Select all text
276    pub fn select_all(&mut self) {
277        self.selection_start = Some(0);
278        self.selection_end = Some(self.buffer.len());
279        self.cursor = self.buffer.len();
280    }
281
282    /// Get selected text
283    pub fn selected_text(&self) -> Option<&str> {
284        match (self.selection_start, self.selection_end) {
285            (Some(start), Some(end)) if start != end => {
286                let (s, e) = if start < end {
287                    (start, end)
288                } else {
289                    (end, start)
290                };
291                Some(&self.buffer[s..e])
292            }
293            _ => None,
294        }
295    }
296
297    /// Delete selected text
298    pub fn delete_selection(&mut self) -> bool {
299        match (self.selection_start, self.selection_end) {
300            (Some(start), Some(end)) if start != end => {
301                let (s, e) = if start < end {
302                    (start, end)
303                } else {
304                    (end, start)
305                };
306                self.buffer.drain(s..e);
307                self.cursor = s;
308                self.clear_selection();
309                true
310            }
311            _ => false,
312        }
313    }
314
315    /// Clear selection
316    pub fn clear_selection(&mut self) {
317        self.selection_start = None;
318        self.selection_end = None;
319    }
320
321    /// Handle a key event. Returns true if text changed.
322    pub fn handle_key(&mut self, key_code: u32, char_code: u32, modifiers: u8) -> bool {
323        let ctrl = modifiers & 2 != 0;
324
325        // Ctrl+A = select all
326        if ctrl && (key_code == 65 || key_code == 97) {
327            self.select_all();
328            return false;
329        }
330
331        match key_code {
332            8 => self.backspace(), // Backspace
333            46 => self.delete(),   // Delete
334            37 => {
335                // Left arrow
336                self.move_left();
337                false
338            }
339            39 => {
340                // Right arrow
341                self.move_right();
342                false
343            }
344            36 => {
345                // Home
346                self.move_home();
347                false
348            }
349            35 => {
350                // End
351                self.move_end();
352                false
353            }
354            _ => {
355                // Printable character
356                if (32..127).contains(&char_code) {
357                    if let Some(ch) = char::from_u32(char_code) {
358                        self.insert_char(ch);
359                        return true;
360                    }
361                }
362                false
363            }
364        }
365    }
366
367    /// Get the display string with cursor marker for rendering.
368    /// Returns (text_before_cursor, text_after_cursor).
369    pub fn render_parts(&self) -> (&str, &str) {
370        let pos = self.cursor.min(self.buffer.len());
371        (&self.buffer[..pos], &self.buffer[pos..])
372    }
373
374    // -- helpers --
375
376    fn prev_char_boundary(&self, pos: usize) -> usize {
377        let mut p = pos.saturating_sub(1);
378        while p > 0 && !self.buffer.is_char_boundary(p) {
379            p -= 1;
380        }
381        p
382    }
383
384    fn next_char_boundary(&self, pos: usize) -> usize {
385        let mut p = pos + 1;
386        while p < self.buffer.len() && !self.buffer.is_char_boundary(p) {
387            p += 1;
388        }
389        p.min(self.buffer.len())
390    }
391}
392
393// ---------------------------------------------------------------------------
394// Link handling
395// ---------------------------------------------------------------------------
396
397/// Result of clicking on a link
398#[derive(Debug, Clone)]
399pub enum LinkAction {
400    /// Navigate to a new URL
401    Navigate(String),
402    /// Navigate within the same page (anchor)
403    ScrollToAnchor(String),
404    /// JavaScript URL
405    JavaScript(String),
406    /// No action
407    None,
408}
409
410/// Extract href from a link node and determine navigation action
411pub fn handle_click_on_link(href: &str) -> LinkAction {
412    if href.is_empty() {
413        return LinkAction::None;
414    }
415    if let Some(anchor) = href.strip_prefix('#') {
416        return LinkAction::ScrollToAnchor(anchor.to_string());
417    }
418    if let Some(js) = href.strip_prefix("javascript:") {
419        return LinkAction::JavaScript(js.to_string());
420    }
421    LinkAction::Navigate(href.to_string())
422}
423
424// ---------------------------------------------------------------------------
425// Scroll state
426// ---------------------------------------------------------------------------
427
428/// Viewport scroll state
429#[derive(Debug, Clone, Default)]
430pub struct ScrollState {
431    /// Current vertical scroll offset (pixels)
432    pub scroll_y: i32,
433    /// Maximum scroll offset (content_height - viewport_height)
434    pub max_scroll_y: i32,
435    /// Viewport height (pixels)
436    pub viewport_height: i32,
437    /// Total content height (pixels)
438    pub content_height: i32,
439    /// Horizontal scroll offset (pixels)
440    pub scroll_x: i32,
441    /// Maximum horizontal scroll
442    pub max_scroll_x: i32,
443    /// Viewport width
444    pub viewport_width: i32,
445    /// Total content width
446    pub content_width: i32,
447}
448
449impl ScrollState {
450    pub fn new(viewport_width: i32, viewport_height: i32) -> Self {
451        Self {
452            viewport_width,
453            viewport_height,
454            ..Default::default()
455        }
456    }
457
458    /// Update content dimensions and recalculate max scroll
459    pub fn set_content_size(&mut self, width: i32, height: i32) {
460        self.content_width = width;
461        self.content_height = height;
462        self.max_scroll_y = (height - self.viewport_height).max(0);
463        self.max_scroll_x = (width - self.viewport_width).max(0);
464        self.clamp();
465    }
466
467    /// Update viewport dimensions
468    pub fn set_viewport_size(&mut self, width: i32, height: i32) {
469        self.viewport_width = width;
470        self.viewport_height = height;
471        self.max_scroll_y = (self.content_height - height).max(0);
472        self.max_scroll_x = (self.content_width - width).max(0);
473        self.clamp();
474    }
475
476    /// Scroll by a delta (positive = down/right)
477    pub fn scroll_by(&mut self, dx: i32, dy: i32) {
478        self.scroll_x += dx;
479        self.scroll_y += dy;
480        self.clamp();
481    }
482
483    /// Scroll to an absolute position
484    pub fn scroll_to(&mut self, x: i32, y: i32) {
485        self.scroll_x = x;
486        self.scroll_y = y;
487        self.clamp();
488    }
489
490    /// Scroll to make a vertical position visible
491    pub fn ensure_visible_y(&mut self, y: i32, height: i32) {
492        if y < self.scroll_y {
493            self.scroll_y = y;
494        } else if y + height > self.scroll_y + self.viewport_height {
495            self.scroll_y = y + height - self.viewport_height;
496        }
497        self.clamp();
498    }
499
500    /// Whether a vertical scrollbar is needed
501    pub fn needs_v_scrollbar(&self) -> bool {
502        self.content_height > self.viewport_height
503    }
504
505    /// Whether a horizontal scrollbar is needed
506    pub fn needs_h_scrollbar(&self) -> bool {
507        self.content_width > self.viewport_width
508    }
509
510    /// Get scrollbar thumb position and size for vertical scrollbar.
511    /// Returns (thumb_y, thumb_height) in pixels within the track.
512    pub fn v_scrollbar_thumb(&self, track_height: i32) -> (i32, i32) {
513        if self.content_height <= 0 || !self.needs_v_scrollbar() {
514            return (0, track_height);
515        }
516        let thumb_h =
517            (self.viewport_height as i64 * track_height as i64 / self.content_height as i64) as i32;
518        let thumb_h = thumb_h.max(20); // minimum thumb size
519        let scrollable = track_height - thumb_h;
520        let thumb_y = if self.max_scroll_y > 0 {
521            (self.scroll_y as i64 * scrollable as i64 / self.max_scroll_y as i64) as i32
522        } else {
523            0
524        };
525        (thumb_y, thumb_h)
526    }
527
528    fn clamp(&mut self) {
529        self.scroll_x = self.scroll_x.clamp(0, self.max_scroll_x);
530        self.scroll_y = self.scroll_y.clamp(0, self.max_scroll_y);
531    }
532}
533
534// ---------------------------------------------------------------------------
535// URL encoding helper
536// ---------------------------------------------------------------------------
537
538/// Simple URL encoding (percent-encoding)
539fn url_encode(s: &str) -> String {
540    let mut result = String::with_capacity(s.len());
541    for b in s.bytes() {
542        match b {
543            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
544                result.push(b as char);
545            }
546            b' ' => result.push('+'),
547            _ => {
548                result.push('%');
549                result.push(hex_upper((b >> 4) & 0x0F));
550                result.push(hex_upper(b & 0x0F));
551            }
552        }
553    }
554    result
555}
556
557fn hex_upper(n: u8) -> char {
558    if n < 10 {
559        (b'0' + n) as char
560    } else {
561        (b'A' + n - 10) as char
562    }
563}
564
565// ---------------------------------------------------------------------------
566// Tests
567// ---------------------------------------------------------------------------
568
569#[cfg(test)]
570mod tests {
571    #[allow(unused_imports)]
572    use alloc::vec;
573
574    use super::*;
575
576    #[test]
577    fn test_text_input_insert() {
578        let mut ti = TextInput::new();
579        ti.insert_char('h');
580        ti.insert_char('i');
581        assert_eq!(ti.buffer, "hi");
582        assert_eq!(ti.cursor, 2);
583    }
584
585    #[test]
586    fn test_text_input_insert_str() {
587        let mut ti = TextInput::new();
588        ti.insert_str("hello");
589        assert_eq!(ti.buffer, "hello");
590        assert_eq!(ti.cursor, 5);
591    }
592
593    #[test]
594    fn test_text_input_backspace() {
595        let mut ti = TextInput::from_text("abc");
596        assert!(ti.backspace());
597        assert_eq!(ti.buffer, "ab");
598        assert_eq!(ti.cursor, 2);
599    }
600
601    #[test]
602    fn test_text_input_backspace_empty() {
603        let mut ti = TextInput::new();
604        assert!(!ti.backspace());
605    }
606
607    #[test]
608    fn test_text_input_delete() {
609        let mut ti = TextInput::from_text("abc");
610        ti.cursor = 0;
611        assert!(ti.delete());
612        assert_eq!(ti.buffer, "bc");
613    }
614
615    #[test]
616    fn test_text_input_delete_at_end() {
617        let mut ti = TextInput::from_text("abc");
618        assert!(!ti.delete());
619    }
620
621    #[test]
622    fn test_text_input_move_left_right() {
623        let mut ti = TextInput::from_text("abc");
624        ti.move_left();
625        assert_eq!(ti.cursor, 2);
626        ti.move_left();
627        assert_eq!(ti.cursor, 1);
628        ti.move_right();
629        assert_eq!(ti.cursor, 2);
630    }
631
632    #[test]
633    fn test_text_input_home_end() {
634        let mut ti = TextInput::from_text("hello");
635        ti.move_home();
636        assert_eq!(ti.cursor, 0);
637        ti.move_end();
638        assert_eq!(ti.cursor, 5);
639    }
640
641    #[test]
642    fn test_text_input_select_all() {
643        let mut ti = TextInput::from_text("hello");
644        ti.select_all();
645        assert_eq!(ti.selection_start, Some(0));
646        assert_eq!(ti.selection_end, Some(5));
647        assert_eq!(ti.selected_text(), Some("hello"));
648    }
649
650    #[test]
651    fn test_text_input_delete_selection() {
652        let mut ti = TextInput::from_text("hello world");
653        ti.selection_start = Some(5);
654        ti.selection_end = Some(11);
655        assert!(ti.delete_selection());
656        assert_eq!(ti.buffer, "hello");
657        assert_eq!(ti.cursor, 5);
658    }
659
660    #[test]
661    fn test_text_input_render_parts() {
662        let mut ti = TextInput::from_text("hello");
663        ti.cursor = 3;
664        let (before, after) = ti.render_parts();
665        assert_eq!(before, "hel");
666        assert_eq!(after, "lo");
667    }
668
669    #[test]
670    fn test_input_element_password_display() {
671        let mut ie = InputElement::new(0, InputType::Password, "pw");
672        ie.set_value("secret");
673        assert_eq!(ie.display_text(), "******");
674    }
675
676    #[test]
677    fn test_input_element_placeholder() {
678        let mut ie = InputElement::new(0, InputType::Text, "name");
679        ie.placeholder = "Enter name".to_string();
680        assert_eq!(ie.display_text(), "Enter name");
681        ie.set_value("Alice");
682        assert_eq!(ie.display_text(), "Alice");
683    }
684
685    #[test]
686    fn test_input_element_toggle_checkbox() {
687        let mut ie = InputElement::new(0, InputType::Checkbox, "agree");
688        assert!(!ie.checked);
689        ie.toggle_checked();
690        assert!(ie.checked);
691        ie.toggle_checked();
692        assert!(!ie.checked);
693    }
694
695    #[test]
696    fn test_form_encode() {
697        let form = FormElement::new(0, "/submit", FormMethod::Post);
698        let inputs = vec![
699            {
700                let mut ie = InputElement::new(1, InputType::Text, "user");
701                ie.set_value("alice");
702                ie
703            },
704            {
705                let mut ie = InputElement::new(2, InputType::Text, "pass");
706                ie.set_value("s&cr=t");
707                ie
708            },
709        ];
710        let encoded = form.encode_form_data(&inputs);
711        assert!(encoded.contains("user=alice"));
712        assert!(encoded.contains("pass=s%26cr%3Dt"));
713    }
714
715    #[test]
716    fn test_link_navigate() {
717        match handle_click_on_link("https://example.com") {
718            LinkAction::Navigate(url) => assert_eq!(url, "https://example.com"),
719            _ => panic!("expected Navigate"),
720        }
721    }
722
723    #[test]
724    fn test_link_anchor() {
725        match handle_click_on_link("#top") {
726            LinkAction::ScrollToAnchor(a) => assert_eq!(a, "top"),
727            _ => panic!("expected ScrollToAnchor"),
728        }
729    }
730
731    #[test]
732    fn test_link_javascript() {
733        match handle_click_on_link("javascript:alert(1)") {
734            LinkAction::JavaScript(js) => assert_eq!(js, "alert(1)"),
735            _ => panic!("expected JavaScript"),
736        }
737    }
738
739    #[test]
740    fn test_link_empty() {
741        match handle_click_on_link("") {
742            LinkAction::None => {}
743            _ => panic!("expected None"),
744        }
745    }
746
747    #[test]
748    fn test_scroll_state_basic() {
749        let mut s = ScrollState::new(800, 600);
750        s.set_content_size(800, 1200);
751        assert_eq!(s.max_scroll_y, 600);
752        assert!(s.needs_v_scrollbar());
753        assert!(!s.needs_h_scrollbar());
754    }
755
756    #[test]
757    fn test_scroll_by() {
758        let mut s = ScrollState::new(800, 600);
759        s.set_content_size(800, 1200);
760        s.scroll_by(0, 100);
761        assert_eq!(s.scroll_y, 100);
762        s.scroll_by(0, 1000);
763        assert_eq!(s.scroll_y, 600); // clamped
764    }
765
766    #[test]
767    fn test_scroll_to() {
768        let mut s = ScrollState::new(800, 600);
769        s.set_content_size(800, 1200);
770        s.scroll_to(0, 300);
771        assert_eq!(s.scroll_y, 300);
772        s.scroll_to(0, -10);
773        assert_eq!(s.scroll_y, 0); // clamped
774    }
775
776    #[test]
777    fn test_scrollbar_thumb() {
778        let mut s = ScrollState::new(800, 600);
779        s.set_content_size(800, 1200);
780        let (y, h) = s.v_scrollbar_thumb(600);
781        // thumb should be about half the track (600/1200)
782        assert_eq!(h, 300);
783        assert_eq!(y, 0);
784    }
785
786    #[test]
787    fn test_url_encode() {
788        assert_eq!(url_encode("hello"), "hello");
789        assert_eq!(url_encode("a b"), "a+b");
790        assert_eq!(url_encode("a&b=c"), "a%26b%3Dc");
791    }
792
793    #[test]
794    fn test_handle_key_printable() {
795        let mut ti = TextInput::new();
796        let changed = ti.handle_key(65, 65, 0); // 'A'
797        assert!(changed);
798        assert_eq!(ti.buffer, "A");
799    }
800
801    #[test]
802    fn test_handle_key_backspace() {
803        let mut ti = TextInput::from_text("ab");
804        let changed = ti.handle_key(8, 0, 0);
805        assert!(changed);
806        assert_eq!(ti.buffer, "a");
807    }
808}