From 63e59c0544328b9913dea013282627454a547ac4 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Thu, 26 Mar 2026 07:32:55 +0900 Subject: [PATCH] feat(editor): add text input, scroll panel, drag-and-drop widgets Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/voltex_editor/src/draw_list.rs | 31 ++ crates/voltex_editor/src/renderer.rs | 7 +- crates/voltex_editor/src/ui_context.rs | 65 +++- crates/voltex_editor/src/widgets.rs | 468 ++++++++++++++++++++++++- 4 files changed, 567 insertions(+), 4 deletions(-) diff --git a/crates/voltex_editor/src/draw_list.rs b/crates/voltex_editor/src/draw_list.rs index 51fce06..5ff3acf 100644 --- a/crates/voltex_editor/src/draw_list.rs +++ b/crates/voltex_editor/src/draw_list.rs @@ -9,15 +9,27 @@ pub struct DrawVertex { pub color: [u8; 4], } +/// A scissor rectangle for content clipping, in pixel coordinates. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ScissorRect { + pub x: u32, + pub y: u32, + pub w: u32, + pub h: u32, +} + pub struct DrawCommand { pub index_offset: u32, pub index_count: u32, + /// Optional scissor rect for clipping. None means no clipping. + pub scissor: Option, } pub struct DrawList { pub vertices: Vec, pub indices: Vec, pub commands: Vec, + scissor_stack: Vec, } impl DrawList { @@ -26,6 +38,7 @@ impl DrawList { vertices: Vec::new(), indices: Vec::new(), commands: Vec::new(), + scissor_stack: Vec::new(), } } @@ -33,6 +46,23 @@ impl DrawList { self.vertices.clear(); self.indices.clear(); self.commands.clear(); + self.scissor_stack.clear(); + } + + /// Push a scissor rect onto the stack. All subsequent draw commands will + /// be clipped to this rectangle until `pop_scissor` is called. + pub fn push_scissor(&mut self, x: u32, y: u32, w: u32, h: u32) { + self.scissor_stack.push(ScissorRect { x, y, w, h }); + } + + /// Pop the current scissor rect from the stack. + pub fn pop_scissor(&mut self) { + self.scissor_stack.pop(); + } + + /// Returns the current scissor rect (top of stack), or None. + fn current_scissor(&self) -> Option { + self.scissor_stack.last().copied() } /// Add a solid-color rectangle. UV is (0,0) for solid color rendering. @@ -67,6 +97,7 @@ impl DrawList { self.commands.push(DrawCommand { index_offset, index_count: 6, + scissor: self.current_scissor(), }); } diff --git a/crates/voltex_editor/src/renderer.rs b/crates/voltex_editor/src/renderer.rs index 916458d..5dbf4c2 100644 --- a/crates/voltex_editor/src/renderer.rs +++ b/crates/voltex_editor/src/renderer.rs @@ -286,8 +286,13 @@ impl UiRenderer { pass.set_vertex_buffer(0, vertex_buffer.slice(..)); pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16); - // Draw each command + // Draw each command (with optional scissor clipping) for cmd in &draw_list.commands { + if let Some(scissor) = &cmd.scissor { + pass.set_scissor_rect(scissor.x, scissor.y, scissor.w, scissor.h); + } else { + pass.set_scissor_rect(0, 0, screen_w as u32, screen_h as u32); + } pass.draw_indexed( cmd.index_offset..cmd.index_offset + cmd.index_count, 0, diff --git a/crates/voltex_editor/src/ui_context.rs b/crates/voltex_editor/src/ui_context.rs index 8212718..814e6b7 100644 --- a/crates/voltex_editor/src/ui_context.rs +++ b/crates/voltex_editor/src/ui_context.rs @@ -1,7 +1,19 @@ +use std::collections::HashMap; use crate::draw_list::DrawList; use crate::font::FontAtlas; use crate::layout::LayoutState; +/// Key events the UI system understands. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Key { + Left, + Right, + Backspace, + Delete, + Home, + End, +} + pub struct UiContext { pub hot: Option, pub active: Option, @@ -12,11 +24,23 @@ pub struct UiContext { pub mouse_down: bool, pub mouse_clicked: bool, pub mouse_released: bool, + pub mouse_scroll: f32, pub screen_width: f32, pub screen_height: f32, pub font: FontAtlas, id_counter: u64, prev_mouse_down: bool, + // Text input state + pub focused_id: Option, + pub cursor_pos: usize, + input_chars: Vec, + input_keys: Vec, + // Scroll panel state + pub scroll_offsets: HashMap, + // Drag and drop state + pub dragging: Option<(u32, u64)>, + pub drag_start: (f32, f32), + pub(crate) drag_started: bool, } impl UiContext { @@ -32,11 +56,20 @@ impl UiContext { mouse_down: false, mouse_clicked: false, mouse_released: false, + mouse_scroll: 0.0, screen_width: screen_w, screen_height: screen_h, font: FontAtlas::generate(), id_counter: 0, prev_mouse_down: false, + focused_id: None, + cursor_pos: 0, + input_chars: Vec::new(), + input_keys: Vec::new(), + scroll_offsets: HashMap::new(), + dragging: None, + drag_start: (0.0, 0.0), + drag_started: false, } } @@ -60,9 +93,39 @@ impl UiContext { self.layout = LayoutState::new(0.0, 0.0); } + /// Feed a character input event (printable ASCII) for text input widgets. + pub fn input_char(&mut self, ch: char) { + if ch.is_ascii() && !ch.is_ascii_control() { + self.input_chars.push(ch); + } + } + + /// Feed a key input event for text input widgets. + pub fn input_key(&mut self, key: Key) { + self.input_keys.push(key); + } + + /// Set mouse scroll delta for this frame (positive = scroll up). + pub fn set_scroll(&mut self, delta: f32) { + self.mouse_scroll = delta; + } + + /// Drain all pending input chars (consumed by text_input widget). + pub(crate) fn drain_chars(&mut self) -> Vec { + std::mem::take(&mut self.input_chars) + } + + /// Drain all pending key events (consumed by text_input widget). + pub(crate) fn drain_keys(&mut self) -> Vec { + std::mem::take(&mut self.input_keys) + } + /// End the current frame. pub fn end_frame(&mut self) { - // Nothing for now — GPU submission will hook in here later. + self.mouse_scroll = 0.0; + // Clear any unconsumed input + self.input_chars.clear(); + self.input_keys.clear(); } /// Generate a new unique ID for this frame. diff --git a/crates/voltex_editor/src/widgets.rs b/crates/voltex_editor/src/widgets.rs index 31d5dff..cadbe4d 100644 --- a/crates/voltex_editor/src/widgets.rs +++ b/crates/voltex_editor/src/widgets.rs @@ -1,4 +1,4 @@ -use crate::ui_context::UiContext; +use crate::ui_context::{Key, UiContext}; // Color palette const COLOR_BG: [u8; 4] = [0x2B, 0x2B, 0x2B, 0xFF]; @@ -11,6 +11,13 @@ const COLOR_SLIDER_BG: [u8; 4] = [0x44, 0x44, 0x44, 0xFF]; const COLOR_SLIDER_HANDLE: [u8; 4] = [0x88, 0x88, 0xFF, 0xFF]; const COLOR_CHECK_BG: [u8; 4] = [0x44, 0x44, 0x44, 0xFF]; const COLOR_CHECK_MARK: [u8; 4] = [0x88, 0xFF, 0x88, 0xFF]; +const COLOR_INPUT_BG: [u8; 4] = [0x22, 0x22, 0x22, 0xFF]; +const COLOR_INPUT_BORDER: [u8; 4] = [0x66, 0x66, 0x66, 0xFF]; +const COLOR_INPUT_FOCUSED: [u8; 4] = [0x44, 0x88, 0xFF, 0xFF]; +const COLOR_CURSOR: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF]; +const COLOR_SCROLLBAR_BG: [u8; 4] = [0x33, 0x33, 0x33, 0xFF]; +const COLOR_SCROLLBAR_THUMB: [u8; 4]= [0x66, 0x66, 0x77, 0xFF]; +const DRAG_THRESHOLD: f32 = 5.0; impl UiContext { /// Draw text at the current cursor position and advance to the next line. @@ -233,11 +240,231 @@ impl UiContext { pub fn end_panel(&mut self) { // Nothing for now; future could restore outer cursor state. } + + // ── Text Input Widget ───────────────────────────────────────────── + + /// Draw an editable single-line text input. Returns true if the buffer changed. + /// + /// `id` must be unique per text input. The widget renders a box at (x, y) with + /// the given `width`. Height is determined by the font glyph height + padding. + pub fn text_input(&mut self, id: u32, buffer: &mut String, x: f32, y: f32, width: f32) -> bool { + let gw = self.font.glyph_width as f32; + let gh = self.font.glyph_height as f32; + let padding = self.layout.padding; + let height = gh + padding * 2.0; + + let hovered = self.mouse_in_rect(x, y, width, height); + + // Click to focus / unfocus + if self.mouse_clicked { + if hovered { + self.focused_id = Some(id); + // Place cursor at end or at click position + let click_offset = ((self.mouse_x - x - padding) / gw).round() as usize; + self.cursor_pos = click_offset.min(buffer.len()); + } else if self.focused_id == Some(id) { + self.focused_id = None; + } + } + + let mut changed = false; + + // Process input only if focused + if self.focused_id == Some(id) { + // Ensure cursor_pos is valid + if self.cursor_pos > buffer.len() { + self.cursor_pos = buffer.len(); + } + + // Process character input + let chars = self.drain_chars(); + for ch in chars { + buffer.insert(self.cursor_pos, ch); + self.cursor_pos += 1; + changed = true; + } + + // Process key input + let keys = self.drain_keys(); + for key in keys { + match key { + Key::Backspace => { + if self.cursor_pos > 0 { + buffer.remove(self.cursor_pos - 1); + self.cursor_pos -= 1; + changed = true; + } + } + Key::Delete => { + if self.cursor_pos < buffer.len() { + buffer.remove(self.cursor_pos); + changed = true; + } + } + Key::Left => { + if self.cursor_pos > 0 { + self.cursor_pos -= 1; + } + } + Key::Right => { + if self.cursor_pos < buffer.len() { + self.cursor_pos += 1; + } + } + Key::Home => { + self.cursor_pos = 0; + } + Key::End => { + self.cursor_pos = buffer.len(); + } + } + } + } + + // Draw border + let border_color = if self.focused_id == Some(id) { + COLOR_INPUT_FOCUSED + } else { + COLOR_INPUT_BORDER + }; + self.draw_list.add_rect(x, y, width, height, border_color); + // Draw inner background (1px border) + self.draw_list.add_rect(x + 1.0, y + 1.0, width - 2.0, height - 2.0, COLOR_INPUT_BG); + + // Draw text + let text_x = x + padding; + let text_y = y + padding; + let mut cx = text_x; + for ch in buffer.chars() { + let (u0, v0, u1, v1) = self.font.glyph_uv(ch); + self.draw_list.add_rect_uv(cx, text_y, gw, gh, u0, v0, u1, v1, COLOR_TEXT); + cx += gw; + } + + // Draw cursor if focused + if self.focused_id == Some(id) { + let cursor_x = text_x + self.cursor_pos as f32 * gw; + self.draw_list.add_rect(cursor_x, text_y, 1.0, gh, COLOR_CURSOR); + } + + self.layout.advance_line(); + changed + } + + // ── Scroll Panel ────────────────────────────────────────────────── + + /// Begin a scrollable panel. Content drawn between begin/end will be clipped + /// to the panel bounds. `content_height` is the total height of the content + /// inside the panel (used to compute scrollbar size). + pub fn begin_scroll_panel(&mut self, id: u32, x: f32, y: f32, w: f32, h: f32, content_height: f32) { + let scrollbar_w = 12.0_f32; + let panel_inner_w = w - scrollbar_w; + + // Handle mouse wheel when hovering over the panel + let hovered = self.mouse_in_rect(x, y, w, h); + let scroll_delta = if hovered && self.mouse_scroll.abs() > 0.0 { + -self.mouse_scroll * 20.0 + } else { + 0.0 + }; + + // Get or create scroll offset, apply delta and clamp + let scroll = self.scroll_offsets.entry(id).or_insert(0.0); + *scroll += scroll_delta; + let max_scroll = (content_height - h).max(0.0); + *scroll = scroll.clamp(0.0, max_scroll); + let current_scroll = *scroll; + + // Draw panel background + self.draw_list.add_rect(x, y, w, h, COLOR_PANEL); + + // Draw scrollbar track + let sb_x = x + panel_inner_w; + self.draw_list.add_rect(sb_x, y, scrollbar_w, h, COLOR_SCROLLBAR_BG); + + // Draw scrollbar thumb + if content_height > h { + let thumb_ratio = h / content_height; + let thumb_h = (thumb_ratio * h).max(16.0); + let scroll_ratio = if max_scroll > 0.0 { current_scroll / max_scroll } else { 0.0 }; + let thumb_y = y + scroll_ratio * (h - thumb_h); + self.draw_list.add_rect(sb_x, thumb_y, scrollbar_w, thumb_h, COLOR_SCROLLBAR_THUMB); + } + + // Push scissor rect for content clipping + self.draw_list.push_scissor(x as u32, y as u32, panel_inner_w as u32, h as u32); + + // Set cursor inside panel, offset by scroll + self.layout = crate::layout::LayoutState::new(x + self.layout.padding, y + self.layout.padding - current_scroll); + } + + /// End a scrollable panel. Pops the scissor rect. + pub fn end_scroll_panel(&mut self) { + self.draw_list.pop_scissor(); + } + + // ── Drag and Drop ───────────────────────────────────────────────── + + /// Begin dragging an item. Call this when the user presses down on a draggable element. + /// `id` identifies the source, `payload` is an arbitrary u64 value transferred on drop. + pub fn begin_drag(&mut self, id: u32, payload: u64) { + if self.mouse_clicked { + self.dragging = Some((id, payload)); + self.drag_start = (self.mouse_x, self.mouse_y); + self.drag_started = false; + } + } + + /// Returns true if a drag operation is currently in progress (past the threshold). + pub fn is_dragging(&self) -> bool { + if let Some(_) = self.dragging { + self.drag_started + } else { + false + } + } + + /// End the current drag operation. Returns `Some((source_id, payload))` if a drag + /// was in progress and the mouse was released, otherwise `None`. + pub fn end_drag(&mut self) -> Option<(u32, u64)> { + // Update drag started state based on threshold + if let Some(_) = self.dragging { + if !self.drag_started { + let dx = self.mouse_x - self.drag_start.0; + let dy = self.mouse_y - self.drag_start.1; + if (dx * dx + dy * dy).sqrt() >= DRAG_THRESHOLD { + self.drag_started = true; + } + } + } + + if self.mouse_released { + let result = if self.drag_started { self.dragging } else { None }; + self.dragging = None; + self.drag_started = false; + result + } else { + None + } + } + + /// Declare a drop target region. If a drag is released over this target, + /// returns the payload that was dropped. Otherwise returns `None`. + pub fn drop_target(&mut self, _id: u32, x: f32, y: f32, w: f32, h: f32) -> Option { + if self.mouse_released && self.drag_started { + if self.mouse_in_rect(x, y, w, h) { + if let Some((_src_id, payload)) = self.dragging { + return Some(payload); + } + } + } + None + } } #[cfg(test)] mod tests { - use crate::ui_context::UiContext; + use crate::ui_context::{Key, UiContext}; #[test] fn test_button_returns_false_when_not_clicked() { @@ -278,4 +505,241 @@ mod tests { let v2 = ctx.slider("test", -10.0, 0.0, 100.0); assert!((v2 - 0.0).abs() < 1e-6, "slider should clamp to min: got {}", v2); } + + // ── Text Input Tests ────────────────────────────────────────────── + + #[test] + fn test_text_input_basic_typing() { + let mut ctx = UiContext::new(800.0, 600.0); + let mut buf = String::new(); + + // Click on the text input to focus it (at x=10, y=10, width=200) + ctx.begin_frame(15.0, 15.0, true); + ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0); + + // Now type some characters + ctx.begin_frame(15.0, 15.0, false); + ctx.input_char('H'); + ctx.input_char('i'); + let changed = ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0); + assert!(changed); + assert_eq!(buf, "Hi"); + } + + #[test] + fn test_text_input_backspace() { + let mut ctx = UiContext::new(800.0, 600.0); + let mut buf = String::from("abc"); + + // Focus — click far right so cursor goes to end (padding=4, gw=8, 3 chars → need x > 10+4+24=38) + ctx.begin_frame(50.0, 15.0, true); + ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0); + + // Backspace + ctx.begin_frame(50.0, 15.0, false); + ctx.input_key(Key::Backspace); + let changed = ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0); + assert!(changed); + assert_eq!(buf, "ab"); + } + + #[test] + fn test_text_input_cursor_movement() { + let mut ctx = UiContext::new(800.0, 600.0); + let mut buf = String::from("abc"); + + // Focus — click far right so cursor goes to end + ctx.begin_frame(50.0, 15.0, true); + ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0); + assert_eq!(ctx.cursor_pos, 3); // cursor at end of "abc" + + // Move cursor to beginning with Home + ctx.begin_frame(15.0, 15.0, false); + ctx.input_key(Key::Home); + ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0); + assert_eq!(ctx.cursor_pos, 0); + + // Type 'X' at beginning + ctx.begin_frame(15.0, 15.0, false); + ctx.input_char('X'); + let changed = ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0); + assert!(changed); + assert_eq!(buf, "Xabc"); + assert_eq!(ctx.cursor_pos, 1); + } + + #[test] + fn test_text_input_delete_key() { + let mut ctx = UiContext::new(800.0, 600.0); + let mut buf = String::from("abc"); + + // Focus + ctx.begin_frame(15.0, 15.0, true); + ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0); + + // Move to Home, then Delete + ctx.begin_frame(15.0, 15.0, false); + ctx.input_key(Key::Home); + ctx.input_key(Key::Delete); + let changed = ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0); + assert!(changed); + assert_eq!(buf, "bc"); + } + + #[test] + fn test_text_input_arrow_keys() { + let mut ctx = UiContext::new(800.0, 600.0); + let mut buf = String::from("hello"); + + // Focus — click far right so cursor at end + ctx.begin_frame(100.0, 15.0, true); + ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0); + + // Left twice from end (pos 5→3) + ctx.begin_frame(100.0, 15.0, false); + ctx.input_key(Key::Left); + ctx.input_key(Key::Left); + ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0); + assert_eq!(ctx.cursor_pos, 3); + + // Type 'X' at position 3 + ctx.begin_frame(100.0, 15.0, false); + ctx.input_char('X'); + let changed = ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0); + assert!(changed); + assert_eq!(buf, "helXlo"); + } + + #[test] + fn test_text_input_no_change_when_not_focused() { + let mut ctx = UiContext::new(800.0, 600.0); + let mut buf = String::from("test"); + + // Don't click on the input — mouse at (500, 500) far away + ctx.begin_frame(500.0, 500.0, true); + ctx.input_char('X'); + let changed = ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0); + assert!(!changed); + assert_eq!(buf, "test"); + } + + // ── Scroll Panel Tests ──────────────────────────────────────────── + + #[test] + fn test_scroll_offset_clamping() { + let mut ctx = UiContext::new(800.0, 600.0); + + // Panel at (0,0), 200x100, content_height=300 + // Scroll down a lot + ctx.begin_frame(100.0, 50.0, false); + ctx.set_scroll(-100.0); // scroll down + ctx.begin_scroll_panel(1, 0.0, 0.0, 200.0, 100.0, 300.0); + ctx.end_scroll_panel(); + + // max_scroll = 300 - 100 = 200; scroll should be clamped + let scroll = ctx.scroll_offsets.get(&1).copied().unwrap_or(0.0); + assert!(scroll >= 0.0 && scroll <= 200.0, "scroll={}", scroll); + } + + #[test] + fn test_scroll_offset_does_not_go_negative() { + let mut ctx = UiContext::new(800.0, 600.0); + + // Scroll up when already at top + ctx.begin_frame(100.0, 50.0, false); + ctx.set_scroll(100.0); // scroll up + ctx.begin_scroll_panel(1, 0.0, 0.0, 200.0, 100.0, 300.0); + ctx.end_scroll_panel(); + + let scroll = ctx.scroll_offsets.get(&1).copied().unwrap_or(0.0); + assert!((scroll - 0.0).abs() < 1e-6, "scroll should be 0, got {}", scroll); + } + + #[test] + fn test_scroll_panel_content_clipping() { + let mut ctx = UiContext::new(800.0, 600.0); + + ctx.begin_frame(100.0, 50.0, false); + ctx.begin_scroll_panel(1, 10.0, 20.0, 200.0, 100.0, 300.0); + + // Draw some content inside + ctx.text("Inside scroll"); + // Commands drawn inside should have a scissor rect + let has_scissor = ctx.draw_list.commands.iter().any(|c| c.scissor.is_some()); + assert!(has_scissor, "commands inside scroll panel should have scissor rects"); + + ctx.end_scroll_panel(); + + // Commands drawn after end_scroll_panel should NOT have scissor + let cmds_before = ctx.draw_list.commands.len(); + ctx.text("Outside scroll"); + let new_cmds = &ctx.draw_list.commands[cmds_before..]; + let has_scissor_after = new_cmds.iter().any(|c| c.scissor.is_some()); + assert!(!has_scissor_after, "commands after end_scroll_panel should not have scissor"); + } + + // ── Drag and Drop Tests ────────────────────────────────────────── + + #[test] + fn test_drag_start_and_end() { + let mut ctx = UiContext::new(800.0, 600.0); + + // Frame 1: mouse down — begin drag + ctx.begin_frame(100.0, 100.0, true); + ctx.begin_drag(1, 42); + assert!(!ctx.is_dragging(), "should not be dragging yet (below threshold)"); + let _ = ctx.end_drag(); + + // Frame 2: mouse moved past threshold, still down + ctx.begin_frame(110.0, 100.0, true); + let _ = ctx.end_drag(); + assert!(ctx.is_dragging(), "should be dragging after moving past threshold"); + + // Frame 3: mouse released + ctx.begin_frame(120.0, 100.0, false); + let result = ctx.end_drag(); + assert!(result.is_some()); + let (src_id, payload) = result.unwrap(); + assert_eq!(src_id, 1); + assert_eq!(payload, 42); + } + + #[test] + fn test_drop_on_target() { + let mut ctx = UiContext::new(800.0, 600.0); + + // Frame 1: begin drag + ctx.begin_frame(100.0, 100.0, true); + ctx.begin_drag(1, 99); + let _ = ctx.end_drag(); + + // Frame 2: move past threshold + ctx.begin_frame(110.0, 100.0, true); + let _ = ctx.end_drag(); + + // Frame 3: release over drop target at (200, 200, 50, 50) + ctx.begin_frame(220.0, 220.0, false); + let drop_result = ctx.drop_target(2, 200.0, 200.0, 50.0, 50.0); + assert_eq!(drop_result, Some(99)); + let _ = ctx.end_drag(); + } + + #[test] + fn test_drop_outside_target() { + let mut ctx = UiContext::new(800.0, 600.0); + + // Frame 1: begin drag + ctx.begin_frame(100.0, 100.0, true); + ctx.begin_drag(1, 77); + let _ = ctx.end_drag(); + + // Frame 2: move past threshold + ctx.begin_frame(110.0, 100.0, true); + let _ = ctx.end_drag(); + + // Frame 3: release far from drop target + ctx.begin_frame(500.0, 500.0, false); + let drop_result = ctx.drop_target(2, 200.0, 200.0, 50.0, 50.0); + assert_eq!(drop_result, None); + } }