1use std::sync::Arc;
14
15use ui_layout::Style;
16use url::Url;
17use winit::event::{ElementState, Ime, KeyEvent};
18
19use crate::browser::Tab;
20use crate::browser::core::resource_loader::{BrowserNetworkError, BrowserResponse};
21use crate::browser::core::tab::{FetchKind, TabTask};
22use crate::browser::core::ui::chrome::{Chrome, ChromeAction, ChromeEventResult, DEVTOOLS_URL};
23use crate::browser::core::ui::{
24 FetchRequest, logical_key_to_special_event, logical_key_to_text_key,
25};
26use crate::engine::bridge::text::TextMeasurer;
27use crate::engine::html::ScriptingMode;
28use crate::engine::layouter::types::{Color, TextFlowStyle, TextStyle};
29use crate::engine::renderer_model::{
30 AffineTransform, Brush, DrawCommand, FillRule, Paint, Rect, rect_path,
31};
32use crate::engine::ui::button::ButtonComponent;
33use crate::engine::ui::custom_node::{ContentSize, CustomNode, PointerEvent};
34use crate::engine::ui::input_text::InputTextComponent;
35use crate::engine::ui::input_text_types::InputTextEvent;
36use crate::platform::renderer::text_measurer::PlatformTextMeasurer;
37
38const CHROME_PADDING: f32 = 8.0;
40const CHROME_GAP: f32 = 8.0;
42const TOOLBAR_BACKGROUND: Color = Color(210, 210, 214, 255);
44const BUTTON_BACKGROUND: Color = Color(240, 240, 240, 255);
46const LABEL_COLOR: Color = Color(20, 20, 20, 255);
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51enum ChromeHit {
52 Back,
54 Reload,
56 Scripting,
58 DevTools,
60 DevToolsOrientation,
62 UrlBar,
64}
65
66#[derive(Debug, Clone, Copy)]
68struct ToolbarRects {
69 toolbar: Rect,
71 back: Rect,
73 reload: Rect,
75 scripting: Rect,
77 devtools: Rect,
79 devtools_orientation: Rect,
81 url_bar: Rect,
83}
84
85impl ToolbarRects {
86 fn height(&self) -> f32 {
88 self.toolbar.height
89 }
90}
91
92#[derive(Debug)]
94struct BrowserToolbar {
95 back_button: ButtonComponent,
97 reload_button: ButtonComponent,
99 devtools_button: ButtonComponent,
101 devtools_orientation_button: ButtonComponent,
103 scripting_button: ButtonComponent,
105 url_bar: InputTextComponent,
107}
108
109impl BrowserToolbar {
110 fn new() -> Self {
112 let measurer: Arc<dyn TextMeasurer> = Arc::new(PlatformTextMeasurer::new().unwrap());
113 let back_button = ButtonComponent::new(
114 "← Back",
115 BUTTON_BACKGROUND,
116 LABEL_COLOR,
117 Arc::clone(&measurer),
118 );
119 let reload_button = ButtonComponent::new(
120 "⟳ Reload",
121 BUTTON_BACKGROUND,
122 LABEL_COLOR,
123 Arc::clone(&measurer),
124 );
125 let scripting_button = ButtonComponent::new(
126 display_scripting(&ScriptingMode::default()),
127 BUTTON_BACKGROUND,
128 LABEL_COLOR,
129 Arc::clone(&measurer),
130 );
131 let devtools_button = ButtonComponent::new(
132 "DevTools",
133 BUTTON_BACKGROUND,
134 LABEL_COLOR,
135 Arc::clone(&measurer),
136 );
137 let devtools_orientation_button =
138 ButtonComponent::new("↔", BUTTON_BACKGROUND, LABEL_COLOR, Arc::clone(&measurer));
139 let url_bar = InputTextComponent::new("", "Enter URL", measurer);
140 Self {
141 back_button,
142 reload_button,
143 scripting_button,
144 devtools_button,
145 devtools_orientation_button,
146 url_bar,
147 }
148 }
149
150 fn rects(&self, width: f32) -> ToolbarRects {
152 let back_size = self.back_button.intrinsic_size();
153 let reload_size = self.reload_button.intrinsic_size();
154 let scripting_size = self.scripting_button.intrinsic_size();
155 let devtools_size = self.devtools_button.intrinsic_size();
156 let devtools_orientation_size = self.devtools_orientation_button.intrinsic_size();
157 let url_size = self.url_bar.intrinsic_size();
158
159 let row_height = [back_size.height, reload_size.height, url_size.height]
160 .into_iter()
161 .fold(0.0, f32::max);
162 let top = CHROME_PADDING;
163 let center_y = |height: f32| top + (row_height - height) * 0.5;
164
165 let back = Rect::new(
166 CHROME_PADDING,
167 center_y(back_size.height),
168 back_size.width,
169 back_size.height,
170 );
171 let reload = Rect::new(
172 back.x + back.width + CHROME_GAP,
173 center_y(reload_size.height),
174 reload_size.width,
175 reload_size.height,
176 );
177 let scripting = Rect::new(
178 reload.x + reload.width + CHROME_GAP,
179 center_y(scripting_size.height),
180 scripting_size.width,
181 scripting_size.height,
182 );
183 let devtools = Rect::new(
184 scripting.x + scripting.width + CHROME_GAP,
185 center_y(devtools_size.height),
186 devtools_size.width,
187 devtools_size.height,
188 );
189 let devtools_orientation = Rect::new(
190 devtools.x + devtools.width + CHROME_GAP,
191 center_y(devtools_orientation_size.height),
192 devtools_orientation_size.width,
193 devtools_orientation_size.height,
194 );
195 let url_x = (devtools_orientation.x + devtools_orientation.width + CHROME_GAP)
196 .min(width - CHROME_PADDING);
197 let url_width = (width - CHROME_PADDING - url_x).max(0.0);
198 let url_bar = Rect::new(url_x, center_y(url_size.height), url_width, url_size.height);
199
200 ToolbarRects {
201 toolbar: Rect::new(0.0, 0.0, width, row_height + CHROME_PADDING * 2.0),
202 back,
203 reload,
204 scripting,
205 devtools,
206 devtools_orientation,
207 url_bar,
208 }
209 }
210
211 fn hit_test(&self, x: f32, y: f32, width: f32) -> Option<ChromeHit> {
214 let rects = self.rects(width);
215 if rects.back.contains(x, y) {
216 Some(ChromeHit::Back)
217 } else if rects.reload.contains(x, y) {
218 Some(ChromeHit::Reload)
219 } else if rects.scripting.contains(x, y) {
220 Some(ChromeHit::Scripting)
221 } else if rects.devtools.contains(x, y) {
222 Some(ChromeHit::DevTools)
223 } else if rects.devtools_orientation.contains(x, y) {
224 Some(ChromeHit::DevToolsOrientation)
225 } else if rects.url_bar.contains(x, y) {
226 Some(ChromeHit::UrlBar)
227 } else {
228 None
229 }
230 }
231}
232
233#[derive(Debug)]
236pub struct BasicChrome {
237 toolbar: BrowserToolbar,
238 last_url: Option<String>,
241
242 is_debug_open: bool,
243 debug_pane: Tab,
244
245 debug_press_active: bool,
249
250 debug_pane_vertical: bool,
252 debug_pane_ratio: f32,
254 debug_dragging: bool,
256 debug_drag_anchor: Option<(f32, f32)>,
258
259 scripting_mode: ScriptingMode,
260 hovered: Option<ChromeHit>,
262}
263
264impl BasicChrome {
265 fn debug_pane_rect(&self, width: f32, height: f32) -> Option<Rect> {
268 if !self.is_debug_open {
269 return None;
270 }
271 let toolbar_h = self.toolbar.rects(width).height();
272 let area_h = height - toolbar_h;
273 if self.debug_pane_vertical {
274 let pane_w = width * self.debug_pane_ratio;
275 Some(Rect::new(width - pane_w, toolbar_h, pane_w, area_h))
276 } else {
277 let pane_h = area_h * self.debug_pane_ratio;
278 Some(Rect::new(0.0, toolbar_h + area_h - pane_h, width, pane_h))
279 }
280 }
281
282 fn divider_rect(&self, width: f32, height: f32) -> Option<Rect> {
284 let pane = self.debug_pane_rect(width, height)?;
285 let half = 4.0;
286 if self.debug_pane_vertical {
287 Some(Rect::new(pane.x - half, pane.y, half * 2.0, pane.height))
288 } else {
289 Some(Rect::new(pane.x, pane.y - half, pane.width, half * 2.0))
290 }
291 }
292
293 pub fn new() -> Self {
295 let mut tab = Tab::default();
296 tab.navigate(DEVTOOLS_URL.parse().unwrap());
297
298 Self {
299 toolbar: BrowserToolbar::new(),
300 last_url: None,
301 is_debug_open: false,
302 debug_pane: tab,
303 debug_press_active: false,
304 debug_pane_vertical: true,
305 debug_pane_ratio: 0.5,
306 debug_dragging: false,
307 debug_drag_anchor: None,
308 scripting_mode: ScriptingMode::default(),
309 hovered: None,
310 }
311 }
312
313 fn dispatch(&self, hit: ChromeHit, event: PointerEvent) -> bool {
317 let node: &dyn CustomNode = match hit {
318 ChromeHit::Back => &self.toolbar.back_button,
319 ChromeHit::Reload => &self.toolbar.reload_button,
320 ChromeHit::Scripting => &self.toolbar.scripting_button,
321 ChromeHit::DevTools => &self.toolbar.devtools_button,
322 ChromeHit::DevToolsOrientation => &self.toolbar.devtools_orientation_button,
323 ChromeHit::UrlBar => &self.toolbar.url_bar,
324 };
325 node.on_pointer_event(event)
326 }
327
328 fn clear_hover(&mut self) {
330 if let Some(previous) = self.hovered.take() {
331 self.dispatch(previous, PointerEvent::Leave);
332 }
333 }
334}
335
336impl Default for BasicChrome {
337 fn default() -> Self {
338 Self::new()
339 }
340}
341
342impl Chrome for BasicChrome {
343 fn content_rect(&self, width: f32, height: f32) -> Rect {
344 let toolbar_height = self.toolbar.rects(width).height();
345 if self.is_debug_open {
346 let area_h = height - toolbar_height;
347 if self.debug_pane_vertical {
348 let pane_w = width * self.debug_pane_ratio;
349 Rect::new(0.0, toolbar_height, width - pane_w, area_h)
350 } else {
351 let pane_h = area_h * self.debug_pane_ratio;
352 Rect::new(0.0, toolbar_height, width, area_h - pane_h)
353 }
354 } else {
355 Rect::new(0.0, toolbar_height, width, height - toolbar_height)
356 }
357 }
358
359 fn draw(&mut self, cmd_buf: &mut Vec<DrawCommand>, width: f32, height: f32) {
360 let rects = self.toolbar.rects(width);
361
362 cmd_buf.push(DrawCommand::Fill {
363 path: rect_path(
364 rects.toolbar.x,
365 rects.toolbar.y,
366 rects.toolbar.width,
367 rects.toolbar.height,
368 ),
369 rule: FillRule::NonZero,
370 paint: Paint {
371 brush: Brush::Solid(TOOLBAR_BACKGROUND),
372 opacity: 1.0,
373 },
374 });
375
376 let text_style = TextStyle::default();
377 let text_flow_style = TextFlowStyle::default();
378 let style = Style::default();
379
380 let components: [(&dyn CustomNode, Rect); 6] = [
381 (&self.toolbar.back_button, rects.back),
382 (&self.toolbar.reload_button, rects.reload),
383 (&self.toolbar.scripting_button, rects.scripting),
384 (&self.toolbar.devtools_button, rects.devtools),
385 (
386 &self.toolbar.devtools_orientation_button,
387 rects.devtools_orientation,
388 ),
389 (&self.toolbar.url_bar, rects.url_bar),
390 ];
391
392 for (node, rect) in components {
393 cmd_buf.push(DrawCommand::PushTransform {
394 transform: AffineTransform::translate(rect.x, rect.y),
395 });
396 node.draw_sized(
397 cmd_buf,
398 &text_style,
399 &text_flow_style,
400 &style,
401 ContentSize {
402 width: rect.width,
403 height: rect.height,
404 },
405 );
406 cmd_buf.push(DrawCommand::PopTransform);
407 }
408
409 if self.is_debug_open {
410 let area_h = height - rects.height();
411 let (pane_x, pane_y, pane_w, pane_h) = if self.debug_pane_vertical {
412 let pane_w = width * self.debug_pane_ratio;
413 (width - pane_w, rects.height(), pane_w, area_h)
414 } else {
415 let pane_h = area_h * self.debug_pane_ratio;
416 (0.0, rects.height() + area_h - pane_h, width, pane_h)
417 };
418
419 cmd_buf.push(DrawCommand::PushTransform {
420 transform: AffineTransform::translate(pane_x, pane_y),
421 });
422
423 let pane_rect = rect_path(0.0, 0.0, pane_w, pane_h);
424 cmd_buf.push(DrawCommand::Fill {
425 path: pane_rect.clone(),
426 rule: FillRule::NonZero,
427 paint: Paint {
428 brush: Brush::Solid(Color(200, 200, 200, 200)),
429 opacity: 1.0,
430 },
431 });
432 cmd_buf.push(DrawCommand::PushClip {
433 path: pane_rect,
434 rule: FillRule::NonZero,
435 });
436
437 self.debug_pane.draw(cmd_buf, pane_w, pane_h);
438
439 cmd_buf.push(DrawCommand::PopClip);
440 cmd_buf.push(DrawCommand::PopTransform);
441
442 let divider_color = Color(160, 160, 160, 255);
444 let divider_w = 4.0;
445 if self.debug_pane_vertical {
446 let dx = pane_x - divider_w * 0.5;
447 cmd_buf.push(DrawCommand::Fill {
448 path: rect_path(dx, rects.height(), divider_w, area_h),
449 rule: FillRule::NonZero,
450 paint: Paint {
451 brush: Brush::Solid(divider_color),
452 opacity: 1.0,
453 },
454 });
455 } else {
456 let dy = pane_y - divider_w * 0.5;
457 cmd_buf.push(DrawCommand::Fill {
458 path: rect_path(0.0, dy, width, divider_w),
459 rule: FillRule::NonZero,
460 paint: Paint {
461 brush: Brush::Solid(divider_color),
462 opacity: 1.0,
463 },
464 });
465 }
466 }
467 }
468
469 fn tick(&mut self, actions_buf: &mut Vec<ChromeAction>) -> (Vec<FetchRequest>, bool) {
470 let mut fetches_buf = Vec::new();
471 let mut redraw = false;
472 for task in self.debug_pane.tick() {
473 match task {
474 TabTask::Fetch { url, kind, origin } => {
475 log::info!("Fetch requested in BasicChrome: url={}", url);
476 fetches_buf.push(FetchRequest { url, kind, origin });
477 }
478 TabTask::NeedsRedraw => redraw = true,
479 TabTask::DevToolsRequest { id, method, params } => {
480 actions_buf.push(ChromeAction::DevToolsRequest { id, method, params });
481 }
482 }
483 }
484
485 (fetches_buf, redraw)
486 }
487
488 fn deliver_fetch(
489 &mut self,
490 kind: FetchKind,
491 url: Url,
492 response: Result<BrowserResponse, BrowserNetworkError>,
493 ) {
494 self.debug_pane.deliver_fetch(kind, url, response);
495 }
496
497 fn sync_url(&mut self, url: Option<&str>) {
498 let url = url.map(str::to_string);
499 if self.last_url != url {
500 self.last_url.clone_from(&url);
501 self.toolbar.url_bar.set_value(url.unwrap_or_default());
502 }
503 }
504
505 fn pointer_event(
506 &mut self,
507 width: f32,
508 height: f32,
509 event: PointerEvent,
510 state: ElementState,
511 ) -> ChromeEventResult {
512 let (x, y) = match event {
513 PointerEvent::Move { x, y }
514 | PointerEvent::Down { x, y }
515 | PointerEvent::Up { x, y } => (x, y),
516 PointerEvent::Leave => {
517 self.clear_hover();
518 return ChromeEventResult::none();
519 }
520 };
521
522 let Some(hit) = self.toolbar.hit_test(x, y, width) else {
523 self.clear_hover();
526
527 if !self.is_debug_open {
528 return ChromeEventResult::none();
529 }
530
531 let pane = self
532 .debug_pane_rect(width, height)
533 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
534 let divider = self.divider_rect(width, height);
535
536 let on_divider = divider.as_ref().is_some_and(|d| d.contains(x, y));
538
539 if on_divider && let PointerEvent::Down { .. } = event {
541 let anchor = if self.debug_pane_vertical { x } else { y };
542 self.debug_dragging = true;
543 self.debug_drag_anchor = Some((anchor, self.debug_pane_ratio));
544 return ChromeEventResult {
545 consumed: true,
546 action: ChromeAction::None,
547 };
548 }
549
550 if self.debug_dragging {
552 match event {
553 PointerEvent::Move { .. } => {
554 if let Some((anchor, initial_ratio)) = self.debug_drag_anchor {
555 let delta = if self.debug_pane_vertical {
556 x - anchor
557 } else {
558 y - anchor
559 };
560 let area = if self.debug_pane_vertical {
561 width
562 } else {
563 height - self.toolbar.rects(width).height()
564 };
565 let ratio_delta = if area > 0.0 { -delta / area } else { 0.0 };
566 self.debug_pane_ratio = (initial_ratio + ratio_delta).clamp(0.1, 0.9);
567 }
568 return ChromeEventResult {
569 consumed: true,
570 action: ChromeAction::None,
571 };
572 }
573 PointerEvent::Up { .. } => {
574 self.debug_dragging = false;
575 self.debug_drag_anchor = None;
576 return ChromeEventResult {
577 consumed: true,
578 action: ChromeAction::None,
579 };
580 }
581 _ => {}
582 }
583 }
584
585 let inside = pane.contains(x, y);
586
587 if !inside && !self.debug_press_active {
591 return ChromeEventResult::none();
592 }
593
594 let px = x - pane.x;
596 let py = y - pane.y;
597
598 match event {
599 PointerEvent::Move { .. } => {
603 self.debug_pane.handle_pointer_move(px, py);
604 }
605 PointerEvent::Down { .. } => {
606 self.debug_press_active = inside;
607 self.debug_pane.handle_mouse_input(px, py, state);
608 }
609 _ => {
610 self.debug_press_active = false;
611 self.debug_pane.handle_mouse_input(px, py, state);
612 }
613 }
614
615 return ChromeEventResult {
616 consumed: true,
617 action: ChromeAction::None,
618 };
619 };
620
621 if matches!(event, PointerEvent::Up { .. }) && self.debug_press_active {
624 self.debug_press_active = false;
625 if let Some(pane) = self.debug_pane_rect(width, height) {
626 self.debug_pane
627 .handle_mouse_input(x - pane.x, y - pane.y, state);
628 }
629 return ChromeEventResult {
630 consumed: true,
631 action: ChromeAction::None,
632 };
633 }
634
635 let handled = self.dispatch(hit, event);
636 let clicked = matches!(event, PointerEvent::Up { .. }) && handled;
637
638 if matches!(event, PointerEvent::Move { .. })
639 && self.hovered != Some(hit)
640 && let Some(previous) = self.hovered.replace(hit)
641 {
642 self.dispatch(previous, PointerEvent::Leave);
643 }
644
645 let action = match hit {
646 ChromeHit::UrlBar if matches!(event, PointerEvent::Down { .. }) => {
647 self.toolbar.url_bar.set_focused(true);
648 ChromeAction::EnableIme
649 }
650 ChromeHit::Back if clicked => ChromeAction::Back,
651 ChromeHit::Reload if clicked => ChromeAction::Reload,
652 ChromeHit::Scripting if clicked => {
653 self.scripting_mode = if self.scripting_mode == ScriptingMode::Enabled {
654 ScriptingMode::Disabled
655 } else {
656 ScriptingMode::Enabled
657 };
658
659 self.toolbar.scripting_button.label =
660 display_scripting(&self.scripting_mode).into();
661
662 ChromeAction::SetJsPolicy(self.scripting_mode.into())
663 }
664 ChromeHit::DevTools if clicked => {
665 self.is_debug_open = !self.is_debug_open;
666 ChromeAction::None
667 }
668 ChromeHit::DevToolsOrientation if clicked => {
669 self.debug_pane_vertical = !self.debug_pane_vertical;
670 self.toolbar.devtools_orientation_button.label = if self.debug_pane_vertical {
672 "↔"
673 } else {
674 "↕"
675 }
676 .into();
677 ChromeAction::None
678 }
679 _ => ChromeAction::None,
680 };
681
682 ChromeEventResult {
683 consumed: true,
684 action,
685 }
686 }
687
688 fn handle_scroll(
689 &mut self,
690 width: f32,
691 height: f32,
692 mouse_x: f32,
693 mouse_y: f32,
694 scroll_x: f32,
695 scroll_y: f32,
696 ) {
697 if let Some(rect) = self.debug_pane_rect(width, height) {
698 let toolbar_h = self.toolbar.rects(width).height();
699 if rect.contains(mouse_x, mouse_y + toolbar_h) {
700 self.debug_pane.scroll_at(
701 mouse_x - rect.x,
702 mouse_y,
703 scroll_x,
704 scroll_y,
705 (rect.width, rect.height),
706 );
707 }
708 }
709 }
710
711 fn accepts_text_input(&self) -> bool {
712 self.toolbar.url_bar.is_focused()
713 }
714
715 fn key_event(&mut self, event: &KeyEvent, ctrl: bool) -> ChromeAction {
716 if let winit::keyboard::Key::Named(winit::keyboard::NamedKey::Enter) = &event.logical_key {
717 let url = self.toolbar.url_bar.state().value;
718 self.toolbar
719 .url_bar
720 .handle_text_input(InputTextEvent::Enter);
721 match Url::parse(&url).or_else(|_| Url::parse(&format!("https://{url}"))) {
722 Ok(url) => ChromeAction::Navigate(url),
723 Err(_) => {
724 log::warn!("Ignoring invalid URL entered in address bar: {}", url);
725 ChromeAction::Repaint
726 }
727 }
728 } else {
729 let special = logical_key_to_special_event(&event.logical_key, ctrl);
730 let key = logical_key_to_text_key(&event.logical_key);
731 let handled = if let Some(special) = special {
732 self.toolbar.url_bar.handle_text_input(special)
733 } else if let Some(key) = key {
734 self.toolbar
735 .url_bar
736 .handle_text_input(InputTextEvent::Key(key))
737 } else if !ctrl && !self.toolbar.url_bar.is_composing() {
738 event.text.as_ref().is_some_and(|text| {
739 self.toolbar
740 .url_bar
741 .handle_text_input(InputTextEvent::Insert(text.to_string()))
742 })
743 } else {
744 false
745 };
746
747 if handled {
748 ChromeAction::Repaint
749 } else {
750 ChromeAction::None
751 }
752 }
753 }
754
755 fn ime_event(&mut self, event: &Ime) -> ChromeAction {
756 let event = match event {
757 Ime::Preedit(text, _) => InputTextEvent::Preedit(text.clone()),
758 Ime::Commit(text) => InputTextEvent::Commit(text.clone()),
759 Ime::Disabled => InputTextEvent::CancelComposition,
760 Ime::Enabled => return ChromeAction::None,
761 };
762
763 if self.toolbar.url_bar.handle_text_input(event) {
764 ChromeAction::Repaint
765 } else {
766 ChromeAction::None
767 }
768 }
769
770 fn on_devtools_response(&mut self, id: u64, result: String) {
771 self.debug_pane.on_devtools_response(id, result);
772 }
773
774 fn blur(&mut self) {
775 self.toolbar.url_bar.set_focused(false);
776 }
777
778 fn needs_repaint(&self) -> bool {
779 self.toolbar.back_button.needs_repaint()
780 || self.toolbar.reload_button.needs_repaint()
781 || self.toolbar.url_bar.needs_repaint()
782 }
783}
784
785fn display_scripting(scripting_mode: &ScriptingMode) -> &'static str {
786 match scripting_mode {
787 ScriptingMode::Enabled => "JS: Enabled ",
788 ScriptingMode::Disabled => "JS: Disabled",
789 }
790}
791
792#[cfg(test)]
793mod tests {
794 use super::*;
795
796 fn click(chrome: &mut BasicChrome, x: f32, y: f32) -> ChromeAction {
798 let down = chrome.pointer_event(
799 800.0,
800 600.0,
801 PointerEvent::Down { x, y },
802 ElementState::Pressed,
803 );
804 assert!(down.consumed);
805
806 let up = chrome.pointer_event(
807 800.0,
808 600.0,
809 PointerEvent::Up { x, y },
810 ElementState::Released,
811 );
812 assert!(up.consumed);
813 up.action
814 }
815
816 fn open_debug_pane(chrome: &mut BasicChrome) -> ToolbarRects {
818 let rects = chrome.toolbar.rects(800.0);
819 let action = click(chrome, rects.devtools.x + 1.0, rects.devtools.y + 1.0);
820 assert_eq!(action, ChromeAction::None);
821 assert!(chrome.is_debug_open);
822 rects
823 }
824
825 #[test]
826 fn toolbar_rects_layout_left_to_right() {
827 let toolbar = BrowserToolbar::new();
828 let rects = toolbar.rects(800.0);
829
830 assert!(rects.back.width > 0.0);
831 assert!(rects.reload.width > 0.0);
832 assert!(rects.url_bar.width > 0.0);
833 assert!(rects.toolbar.width >= 800.0);
834
835 assert!(rects.back.x < rects.reload.x);
837 assert!(rects.reload.x + rects.reload.width < rects.url_bar.x);
838
839 assert!((rects.url_bar.x + rects.url_bar.width + CHROME_PADDING - 800.0).abs() < 0.001);
841 }
842
843 #[test]
844 fn toolbar_rects_fits_narrow_windows() {
845 let toolbar = BrowserToolbar::new();
846 let rects = toolbar.rects(120.0);
847 assert!(rects.toolbar.height > 0.0);
848 assert!(rects.url_bar.x + rects.url_bar.width <= 120.0 + 0.001);
850 }
851
852 #[test]
853 fn pointer_events_hit_chrome_and_content() {
854 let mut chrome = BasicChrome::new();
855 let rects = chrome.toolbar.rects(800.0);
856
857 let result = chrome.pointer_event(
859 800.0,
860 600.0,
861 PointerEvent::Down {
862 x: rects.back.x + 1.0,
863 y: rects.back.y + 1.0,
864 },
865 ElementState::Pressed,
866 );
867 assert!(result.consumed);
868 assert_eq!(result.action, ChromeAction::None);
869
870 let result = chrome.pointer_event(
871 800.0,
872 600.0,
873 PointerEvent::Up {
874 x: rects.back.x + 1.0,
875 y: rects.back.y + 1.0,
876 },
877 ElementState::Released,
878 );
879 assert!(result.consumed);
880 assert_eq!(result.action, ChromeAction::Back);
881
882 let result = chrome.pointer_event(
884 800.0,
885 600.0,
886 PointerEvent::Down {
887 x: rects.url_bar.x + 1.0,
888 y: rects.url_bar.y + 1.0,
889 },
890 ElementState::Pressed,
891 );
892 assert!(result.consumed);
893 assert_eq!(result.action, ChromeAction::EnableIme);
894 assert!(chrome.accepts_text_input());
895
896 let result = chrome.pointer_event(
898 800.0,
899 600.0,
900 PointerEvent::Down {
901 x: 400.0,
902 y: rects.toolbar.height + 10.0,
903 },
904 ElementState::Pressed,
905 );
906 assert!(!result.consumed);
907 assert_eq!(result.action, ChromeAction::None);
908 }
909
910 #[test]
911 fn hovering_tracks_toolbar_elements() {
912 let mut chrome = BasicChrome::new();
913 let rects = chrome.toolbar.rects(800.0);
914
915 let result = chrome.pointer_event(
918 800.0,
919 600.0,
920 PointerEvent::Move {
921 x: rects.back.x + 1.0,
922 y: rects.back.y + 1.0,
923 },
924 ElementState::Released,
925 );
926 assert!(result.consumed);
927
928 let result = chrome.pointer_event(
929 800.0,
930 600.0,
931 PointerEvent::Move {
932 x: rects.reload.x + 1.0,
933 y: rects.reload.y + 1.0,
934 },
935 ElementState::Released,
936 );
937 assert!(result.consumed);
938
939 let result = chrome.pointer_event(
941 800.0,
942 600.0,
943 PointerEvent::Move {
944 x: 400.0,
945 y: rects.toolbar.height + 10.0,
946 },
947 ElementState::Released,
948 );
949 assert!(!result.consumed);
950 assert!(!chrome.toolbar.back_button.is_hovered());
951 assert!(!chrome.toolbar.reload_button.is_hovered());
952 }
953
954 #[test]
955 fn sync_url_updates_address_bar_once() {
956 let mut chrome = BasicChrome::new();
957 chrome.sync_url(Some("https://example.com"));
958 assert_eq!(chrome.toolbar.url_bar.state().value, "https://example.com");
959 chrome
961 .toolbar
962 .url_bar
963 .handle_text_input(InputTextEvent::Insert("zzz".into()));
964 chrome.sync_url(Some("https://example.com"));
965 assert_eq!(
966 chrome.toolbar.url_bar.state().value,
967 "https://example.comzzz"
968 );
969 }
970
971 #[test]
972 fn page_clicks_fall_through_while_debug_pane_is_open() {
973 let mut chrome = BasicChrome::new();
974 open_debug_pane(&mut chrome);
975
976 let down = chrome.pointer_event(
980 800.0,
981 600.0,
982 PointerEvent::Down { x: 200.0, y: 300.0 },
983 ElementState::Pressed,
984 );
985 assert!(!down.consumed);
986
987 let up = chrome.pointer_event(
988 800.0,
989 600.0,
990 PointerEvent::Up { x: 200.0, y: 300.0 },
991 ElementState::Released,
992 );
993 assert!(!up.consumed);
994 }
995
996 #[test]
997 fn debug_pane_click_survives_pointer_moves_and_outside_release() {
998 let mut chrome = BasicChrome::new();
999 let rects = open_debug_pane(&mut chrome);
1000 let pane_x = 800.0 / 2.0;
1001
1002 let down = chrome.pointer_event(
1006 800.0,
1007 600.0,
1008 PointerEvent::Down {
1009 x: pane_x + 100.0,
1010 y: rects.toolbar.height + 50.0,
1011 },
1012 ElementState::Pressed,
1013 );
1014 assert!(down.consumed);
1015
1016 for (dx, dy) in [(1.0, 2.0), (-3.0, 1.0), (2.0, -1.0)] {
1017 let mv = chrome.pointer_event(
1018 800.0,
1019 600.0,
1020 PointerEvent::Move {
1021 x: pane_x + 100.0 + dx,
1022 y: rects.toolbar.height + 50.0 + dy,
1023 },
1024 ElementState::Released,
1025 );
1026 assert!(mv.consumed);
1027 }
1028
1029 let up = chrome.pointer_event(
1030 800.0,
1031 600.0,
1032 PointerEvent::Up {
1033 x: rects.back.x + 1.0,
1034 y: rects.back.y + 1.0,
1035 },
1036 ElementState::Released,
1037 );
1038 assert!(up.consumed);
1039 assert_eq!(up.action, ChromeAction::None);
1040 assert!(!chrome.debug_press_active);
1041
1042 let action = click(&mut chrome, rects.back.x + 1.0, rects.back.y + 1.0);
1044 assert_eq!(action, ChromeAction::Back);
1045 }
1046
1047 mod e2e {
1056 use super::*;
1057 use crate::browser::BrowserCommand;
1058 use crate::browser::core::ui::{BasicContextMenu, BrowserUi, TabId};
1059 use std::time::Duration;
1060
1061 const W: f32 = 1280.0;
1062 const H: f32 = 800.0;
1063
1064 struct Rig {
1065 ui: BrowserUi,
1066 rects: ToolbarRects,
1067 }
1068
1069 fn response(body: &str) -> BrowserResponse {
1070 BrowserResponse {
1071 url: String::new(),
1072 status: hyper::StatusCode::OK.into(),
1073 status_text: "OK".to_string(),
1074 body: body.as_bytes().to_vec(),
1075 headers: vec![],
1076 }
1077 }
1078
1079 fn spin_layout(tab: &mut Tab, w: f32, h: f32) {
1082 let mut buf = Vec::new();
1083 tab.draw(&mut buf, w, h);
1084 for _ in 0..500 {
1085 for _ in tab.tick() {}
1086 if tab.layout_and_info().is_some() {
1087 break;
1088 }
1089 std::thread::sleep(Duration::from_millis(2));
1090 }
1091 tab.draw(&mut buf, w, h);
1092 assert!(tab.layout_and_info().is_some(), "layout must be ready");
1093 }
1094
1095 fn rig(page_body: &str, pane_body: &str) -> Rig {
1099 let rects = BasicChrome::new().toolbar.rects(W);
1100 let toolbar_height = rects.toolbar.height;
1101
1102 let mut tab = Tab::default();
1103 tab.navigate("https://page.test/index.html".parse().unwrap());
1104 tab.on_fetch_succeeded_html(format!("<html><body>{page_body}</body></html>"));
1105 spin_layout(&mut tab, W, H - toolbar_height);
1106
1107 let mut chrome = BasicChrome::new();
1109 let mut actions = Vec::new();
1110 chrome.tick(&mut actions);
1111 chrome.deliver_fetch(
1112 FetchKind::Html,
1113 DEVTOOLS_URL.parse().unwrap(),
1114 Ok(response(&format!("<html><body>{pane_body}</body></html>"))),
1115 );
1116
1117 let mut pane_buf = Vec::new();
1118 for _ in 0..500 {
1119 actions.clear();
1120 let _ = chrome.tick(&mut actions);
1121 if chrome.debug_pane.layout_and_info().is_some() {
1122 break;
1123 }
1124 std::thread::sleep(Duration::from_millis(2));
1125 }
1126 assert!(
1127 chrome.debug_pane.layout_and_info().is_some(),
1128 "pane layout must be ready"
1129 );
1130 chrome
1132 .debug_pane
1133 .draw(&mut pane_buf, W / 2.0, H - toolbar_height);
1134
1135 let ui = BrowserUi::with_tab_and_menu(
1136 tab,
1137 Box::new(chrome),
1138 Box::new(BasicContextMenu::new()),
1139 );
1140 let mut ui = ui;
1141 ui.set_window((W as u32, H as u32), 1.0, "test".into());
1142 Rig { ui, rects }
1143 }
1144
1145 fn cursor_to(rig: &mut Rig, x: f32, y: f32) {
1146 rig.ui.input.mouse_position = (x as f64, y as f64);
1147 rig.ui.handle_pointer_move(x as f64, y as f64);
1148 }
1149
1150 fn press_left(rig: &mut Rig) -> BrowserCommand {
1151 rig.ui
1152 .handle_mouse_input(winit::event::MouseButton::Left, ElementState::Pressed)
1153 }
1154
1155 fn release_left(rig: &mut Rig) -> BrowserCommand {
1156 rig.ui
1157 .handle_mouse_input(winit::event::MouseButton::Left, ElementState::Released)
1158 }
1159
1160 fn open_debug_pane(rig: &mut Rig) {
1161 cursor_to(rig, rig.rects.devtools.x + 1.0, rig.rects.devtools.y + 1.0);
1162 press_left(rig);
1163 release_left(rig);
1164 }
1165
1166 #[test]
1167 fn page_link_press_reaches_active_tab_while_pane_is_open() {
1168 let mut rig = rig(
1169 r#"<a href="https://page.test/target" style="font-size: 24px;">click me please</a>"#,
1170 "<p>devtools</p>",
1171 );
1172 let th = rig.rects.toolbar.height;
1173 open_debug_pane(&mut rig);
1174
1175 cursor_to(&mut rig, 30.0, th + 14.0);
1178 press_left(&mut rig);
1179
1180 let url = rig
1181 .ui
1182 .active_tab()
1183 .and_then(|tab| tab.document_url().map(|u| u.to_string()));
1184 assert_eq!(url.as_deref(), Some("https://page.test/target"));
1185
1186 release_left(&mut rig);
1187 let url = rig
1188 .ui
1189 .active_tab()
1190 .and_then(|tab| tab.document_url().map(|u| u.to_string()))
1191 .unwrap();
1192 assert_eq!(url, "https://page.test/target");
1193 }
1194
1195 #[test]
1196 fn pane_link_press_routes_to_debug_pane_and_spares_the_page() {
1197 let mut rig = rig(
1198 "<p>hello</p>",
1199 r#"<a href="https://pane.test/target" style="font-size: 24px;">pane link</a>"#,
1200 );
1201 let th = rig.rects.toolbar.height;
1202 open_debug_pane(&mut rig);
1203
1204 cursor_to(&mut rig, W / 2.0 + 30.0, th + 14.0);
1207 press_left(&mut rig);
1208
1209 let mut pane_nav = false;
1211 for _ in 0..100 {
1212 let outcome = rig.ui.tick();
1213 pane_nav = outcome.fetches.iter().any(|fetch| {
1214 fetch.tab_id == TabId(0)
1215 && fetch.request.url.as_str() == "https://pane.test/target"
1216 });
1217 if pane_nav {
1218 break;
1219 }
1220 std::thread::sleep(Duration::from_millis(2));
1221 }
1222 assert!(pane_nav, "pane link press must navigate the pane");
1223
1224 for (dx, dy) in [(2.0, 3.0), (-4.0, 1.0), (1.0, -2.0)] {
1225 cursor_to(&mut rig, W / 2.0 + 30.0 + dx, th + 14.0 + dy);
1226 }
1227 release_left(&mut rig);
1228
1229 let url = rig
1231 .ui
1232 .active_tab()
1233 .and_then(|tab| tab.document_url().map(|u| u.to_string()))
1234 .unwrap();
1235 assert_eq!(url, "https://page.test/index.html");
1236 }
1237
1238 #[test]
1239 fn back_button_still_works_while_pane_is_open() {
1240 let mut rig = rig("<p>history page</p>", "<p>devtools</p>");
1241 let th = rig.rects.toolbar.height;
1242
1243 if let Some(tab) = rig.ui.active_tab_mut() {
1245 tab.navigate("https://page.test/second.html".parse().unwrap());
1246 tab.on_fetch_succeeded_html("<html><body><p>second</p></body></html>".into());
1247 spin_layout(tab, W, H - th);
1248 }
1249
1250 open_debug_pane(&mut rig);
1251
1252 let (bx, by) = (rig.rects.back.x + 1.0, rig.rects.back.y + 1.0);
1255 cursor_to(&mut rig, bx, by);
1256 press_left(&mut rig);
1257 release_left(&mut rig);
1258
1259 let url = rig
1260 .ui
1261 .active_tab()
1262 .and_then(|tab| tab.document_url().map(|u| u.to_string()));
1263 assert_eq!(url.as_deref(), Some("https://page.test/index.html"));
1264 }
1265 }
1266}