Files
endorphin-tessera-gui/src/shell/developer_tools.rs
T
2026-10-25 21:25:43 +08:00

1003 lines
34 KiB
Rust

//! Native developer workspace. Coordinates and scroll offsets are logical pixels.
use crate::{
activity::{Activity, Exchange},
app::AppEvent,
display_list::{Color, DisplayList, DrawCommand},
document::{CodeNode, RenderDocument},
geometry::{Point, Rect},
sources::SourceWorkspace,
system_ui::{
Axis, Button, Cursor, Interaction, Role, Target, Ui, WidgetId,
theme::{BAR, BG, BORDER, ERROR, MUTED, OK, PALETTE, TEXT, Theme, readable_accent},
},
text::{self, TextStyle},
};
use std::{
io::{Read, Write},
net::TcpStream,
thread,
time::Duration,
};
use unicode_segmentation::UnicodeSegmentation;
mod console;
mod network;
mod painter;
mod selection;
mod settings;
mod sources;
use painter::*;
use selection::SourceSelection;
const ROW: f32 = 19.0;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Tab {
#[default]
Console,
Sources,
Network,
Settings,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Region {
Transcript = 0,
Tree,
Source,
Requests,
Details,
}
#[derive(Clone, Copy, Debug, Default)]
struct Scroll {
x: f32,
y: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Focus {
Source,
Input,
Filter,
Custom,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Action {
ClearConsole,
ClearNetwork,
Follow,
ResetSession,
Focus(Focus),
Snapshot,
Cloud,
CloudRefresh,
CloudOpen(String),
Node(String, usize),
Request(u64),
Theme(usize),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Split {
Tree,
Network,
}
#[derive(Clone, Copy, Debug)]
enum Drag {
Split(Split),
Scroll(Region, bool, f32),
}
/// Each workspace keeps its own input, focus, selection, scroll and request state.
/// Only appearance and the window-level launcher are shared.
pub struct Workspace {
states: [DeveloperToolsState; 4],
active: usize,
}
impl Workspace {
pub fn new(proxy: winit::event_loop::EventLoopProxy<AppEvent>) -> Self {
let states = [Tab::Console, Tab::Sources, Tab::Network, Tab::Settings].map(|tab| {
DeveloperToolsState {
tab,
proxy: Some(proxy.clone()),
..Default::default()
}
});
Self { states, active: 0 }
}
pub fn select(&mut self, name: Option<&str>) {
let next = match name {
Some("Sources") => 1,
Some("Network") => 2,
Some("Settings") => 3,
_ => 0,
};
let accent = self.states[self.active].accent;
if next != self.active {
self.states[self.active].blur();
}
self.active = next;
self.states[next].accent = accent;
self.states[next].open = name.is_some();
}
}
impl std::ops::Deref for Workspace {
type Target = DeveloperToolsState;
fn deref(&self) -> &Self::Target {
&self.states[self.active]
}
}
impl std::ops::DerefMut for Workspace {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.states[self.active]
}
}
#[derive(Debug)]
pub struct DeveloperToolsState {
pub open: bool,
pointer: Point,
source_selection: SourceSelection,
pub interaction: Interaction,
pub repaint: bool,
pub proxy: Option<winit::event_loop::EventLoopProxy<AppEvent>>,
pub cloud_sources: SourceWorkspace,
pub tab: Tab,
pub selected_node_id: Option<String>,
pub focus: Option<Focus>,
pub input: String,
pub cursor: usize,
pub selection: bool,
pub preedit: String,
pub filter: String,
pub custom: String,
pub accent: Color,
pub input_error: String,
tree_width: f32,
network_fraction: f32,
scroll: [Scroll; 5],
widths: [Option<(u64, f32)>; 5],
reveal_input: bool,
console_stamp: u64,
drag: Option<Drag>,
cloud: bool,
follow: bool,
selected_request: Option<u64>,
console_after: u64,
network_after: u64,
history: Vec<String>,
history_index: Option<usize>,
draft: String,
}
impl Default for DeveloperToolsState {
fn default() -> Self {
Self {
open: false,
pointer: Point::default(),
source_selection: SourceSelection::default(),
interaction: Interaction::default(),
repaint: false,
proxy: None,
cloud_sources: SourceWorkspace::default(),
tab: Tab::Console,
selected_node_id: None,
focus: None,
input: String::new(),
cursor: 0,
selection: false,
preedit: String::new(),
filter: String::new(),
custom: "#1671D5".into(),
accent: PALETTE[0],
input_error: String::new(),
tree_width: 190.0,
network_fraction: 0.42,
scroll: [Scroll::default(); 5],
widths: [None; 5],
reveal_input: true,
console_stamp: 0,
drag: None,
cloud: false,
follow: true,
selected_request: None,
console_after: 0,
network_after: 0,
history: Vec::new(),
history_index: None,
draft: String::new(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct DeveloperToolsLayout {
pub panel: Rect,
pub widgets: Vec<Target<Action>>,
splits: Vec<(Rect, Split)>,
regions: Vec<(Rect, Region, f32, f32)>,
thumbs: Vec<(Rect, Region, bool, f32)>,
input: Option<ConsoleInputLayout>,
source: Option<(Rect, Point, Vec<String>)>,
}
impl DeveloperToolsLayout {
pub fn translate(&mut self, dx: f32, dy: f32) {
self.panel.x += dx;
self.panel.y += dy;
for target in &mut self.widgets {
target.rect.x += dx;
target.rect.y += dy;
}
for (rect, _) in &mut self.splits {
rect.x += dx;
rect.y += dy;
}
for (rect, _, _, _) in &mut self.regions {
rect.x += dx;
rect.y += dy;
}
for (rect, _, _, _) in &mut self.thumbs {
rect.x += dx;
rect.y += dy;
}
if let Some(input) = &mut self.input {
input.viewport.x += dx;
input.viewport.y += dy;
input.origin.x += dx;
input.origin.y += dy;
}
if let Some((area, origin, _)) = &mut self.source {
area.x += dx;
area.y += dy;
origin.x += dx;
origin.y += dy;
}
}
}
#[derive(Clone, Copy, Debug)]
struct ConsoleInputLayout {
viewport: Rect,
origin: Point,
}
impl DeveloperToolsState {
pub fn document_updated(&mut self, doc: &RenderDocument) {
if self
.selected_node_id
.as_deref()
.is_none_or(|id| doc.node(id).is_none())
{
self.selected_node_id = doc.root.as_ref().map(|n| n.id.clone());
}
}
pub fn cursor_kind(&self, layout: &DeveloperToolsLayout) -> winit::window::CursorIcon {
use winit::window::CursorIcon;
if !self.open {
return CursorIcon::Default;
}
if layout
.source
.as_ref()
.is_some_and(|(area, _, _)| area.contains(self.pointer))
{
return CursorIcon::Text;
}
match self.interaction.cursor(&layout.widgets) {
Cursor::Default => CursorIcon::Default,
Cursor::Pointer => CursorIcon::Pointer,
Cursor::Text => CursorIcon::Text,
Cursor::ResizeHorizontal => CursorIcon::ColResize,
Cursor::ResizeVertical => CursorIcon::RowResize,
Cursor::Grab => CursorIcon::Grab,
Cursor::Disabled => CursorIcon::NotAllowed,
}
}
pub fn pointer_left(&mut self) {
self.interaction.pointer_left();
}
pub fn blur(&mut self) {
self.interaction.blur();
self.drag = None;
self.focus = None;
self.preedit.clear();
}
pub fn press(&mut self, layout: &DeveloperToolsLayout, p: Point, activity: &Activity) {
if !self.open || !layout.panel.contains(p) {
return;
}
self.preedit.clear();
self.pointer = p;
self.interaction.pointer_moved(p, &layout.widgets);
let target = self.interaction.press(&layout.widgets);
self.focus = None;
if let Some((area, origin, lines)) = &layout.source
&& area.contains(p)
&& target.is_none()
{
self.focus = Some(Focus::Source);
self.selection = false;
self.source_selection.begin(p, *origin, lines);
}
if let Some(target) = target {
match target.role {
Role::Splitter(_) => {
if let Some((_, split)) = layout.splits.iter().find(|(r, _)| {
Rect::new(r.x - 3.0, r.y - 3.0, r.width + 6.0, r.height + 6.0).contains(p)
}) {
self.drag = Some(Drag::Split(*split));
}
}
Role::Scrollbar(_) => {
if let Some((_, region, h, ratio)) =
layout.thumbs.iter().find(|(r, _, _, _)| r.contains(p))
{
self.drag = Some(Drag::Scroll(*region, *h, *ratio));
}
}
Role::TextField => self.activate(target.action, layout, p, activity),
_ => {}
}
}
}
fn activate(
&mut self,
action: Option<Action>,
layout: &DeveloperToolsLayout,
p: Point,
activity: &Activity,
) {
self.focus = None;
match action {
Some(Action::Focus(f)) => {
self.focus = Some(f);
self.selection = false;
if f == Focus::Input
&& let Some(input) = layout.input
{
let row = (((p.y - input.origin.y) / ROW).floor().max(0.0) as usize)
.min(self.input.split('\n').count() - 1);
let mut start = 0;
let line = self
.input
.split('\n')
.enumerate()
.find_map(|(i, line)| {
if i == row {
Some(line)
} else {
start += line.len() + 1;
None
}
})
.unwrap_or("");
let target = (p.x - input.origin.x).max(0.0);
let mut offset = line.len();
for (i, part) in line.grapheme_indices(true) {
let advance =
text::measure(&line[..i + part.len()], TextStyle::code()).width;
if advance >= target {
offset = i;
break;
}
}
self.cursor = (start + offset).min(self.input.len());
self.reveal_cursor();
}
}
Some(Action::ClearConsole) => {
self.console_after = activity.lock().unwrap().records.back().map_or(0, |r| r.id);
self.scroll[Region::Transcript as usize] = Scroll::default();
self.reveal_cursor();
}
Some(Action::ClearNetwork) => {
self.network_after = activity.lock().unwrap().records.back().map_or(0, |r| r.id);
self.selected_request = None;
}
Some(Action::Follow) => {
self.follow = !self.follow;
self.console_stamp = 0;
}
Some(Action::ResetSession) => self.submit_console(true),
Some(Action::Snapshot) => self.cloud = false,
Some(Action::Cloud) => self.cloud = true,
Some(Action::CloudRefresh) => self.cloud_sources.refresh(),
Some(Action::CloudOpen(id)) => {
self.cloud_sources.open(&id);
self.scroll[Region::Source as usize] = Scroll::default();
}
Some(Action::Node(id, line)) => {
self.selected_node_id = Some(id);
self.scroll[Region::Source as usize].x = 0.0;
self.scroll[Region::Source as usize].y = line.saturating_sub(1) as f32 * ROW;
}
Some(Action::Request(id)) => {
self.selected_request = Some(id);
if self.tab == Tab::Console {
self.tab = Tab::Network;
self.network_after = 0;
self.filter.clear();
}
self.scroll[Region::Details as usize] = Scroll::default();
}
Some(Action::Theme(i)) => {
self.accent = PALETTE[i];
self.custom = format!(
"#{:02X}{:02X}{:02X}",
self.accent.red, self.accent.green, self.accent.blue
);
}
None => {}
}
}
pub fn release(&mut self, layout: &DeveloperToolsLayout, p: Point, activity: &Activity) {
self.pointer = p;
self.interaction.pointer_moved(p, &layout.widgets);
let action = self.interaction.release(&layout.widgets);
self.drag = None;
self.source_selection.dragging = false;
if action.is_some() {
self.activate(action, layout, p, activity);
}
}
pub fn keyboard_control(
&mut self,
event: &winit::event::KeyEvent,
shift: bool,
layout: &DeveloperToolsLayout,
activity: &Activity,
) -> bool {
use winit::{
event::ElementState,
keyboard::{Key, NamedKey},
};
if !self.open || !self.preedit.is_empty() {
return false;
}
if event.logical_key == Key::Named(NamedKey::Escape) && self.focus.is_none() {
if event.state == ElementState::Pressed {
self.interaction.blur();
}
return true;
}
if event.logical_key == Key::Named(NamedKey::Tab)
&& (self.focus != Some(Focus::Input) || shift)
{
if event.state == ElementState::Pressed && !event.repeat {
let target = self.interaction.focus_next(&layout.widgets, shift);
self.focus = target.and_then(|t| match t.action {
Some(Action::Focus(f)) => Some(f),
_ => None,
});
if self.focus == Some(Focus::Input) {
self.cursor = self.input.len();
self.reveal_cursor();
}
}
return true;
}
let control = self
.interaction
.focused(&layout.widgets)
.is_some_and(|t| t.role != Role::TextField);
if control
&& matches!(
event.logical_key,
Key::Named(NamedKey::Enter | NamedKey::Space)
)
{
if event.state == ElementState::Pressed {
if !event.repeat {
self.interaction.key_press(&layout.widgets);
}
} else {
let action = self.interaction.key_release(&layout.widgets);
self.activate(action, layout, Point::default(), activity);
}
return true;
}
false
}
pub fn move_pointer(
&mut self,
p: Point,
delta: Point,
layout: &DeveloperToolsLayout,
_window_width: f32,
) {
self.pointer = p;
self.interaction.pointer_moved(p, &layout.widgets);
if self.source_selection.dragging
&& let Some((area, origin, lines)) = &layout.source
{
if p.y < area.y || p.y > area.y + area.height - 9.0 {
let direction = if p.y < area.y { -ROW } else { ROW };
self.scroll[Region::Source as usize].y =
(self.scroll[Region::Source as usize].y + direction).max(0.0);
}
self.source_selection.extend(p, *origin, lines);
self.repaint = true;
}
match self.drag {
Some(Drag::Split(Split::Tree)) => {
self.tree_width =
(p.x - layout.panel.x).clamp(120.0, (layout.panel.width - 180.0).max(120.0))
}
Some(Drag::Split(Split::Network)) => {
self.network_fraction = ((p.y - layout.panel.y - 92.0)
/ (layout.panel.height - 114.0).max(1.0))
.clamp(0.2, 0.8)
}
Some(Drag::Scroll(r, h, ratio)) => {
let s = &mut self.scroll[r as usize];
if h {
s.x = (s.x + delta.x * ratio).max(0.0);
} else {
s.y = (s.y + delta.y * ratio).max(0.0);
}
if r == Region::Transcript {
self.follow = false;
}
}
None => {}
}
}
pub fn wheel(&mut self, layout: &DeveloperToolsLayout, p: Point, dx: f32, dy: f32) {
if let Some((_, r, _, _)) = layout
.regions
.iter()
.rev()
.find(|(area, _, _, _)| area.contains(p))
{
let s = &mut self.scroll[*r as usize];
s.x = (s.x - dx).max(0.0);
s.y = (s.y - dy).max(0.0);
if *r == Region::Transcript {
self.follow = false;
}
}
}
pub fn insert(&mut self, value: &str) {
match self.focus {
Some(Focus::Input) => {
if self.input.len() + value.len() > 1_048_576 {
self.input_error = "Input exceeds 1 MiB".into();
return;
}
if self.selection {
self.input.clear();
self.cursor = 0;
self.selection = false;
}
self.input.insert_str(self.cursor, value);
self.cursor += value.len();
self.history_index = None;
self.reveal_cursor();
}
Some(Focus::Filter) => {
if self.selection {
self.filter.clear();
self.selection = false;
}
self.filter
.extend(value.chars().filter(|c| !c.is_control()).take(200));
self.scroll[Region::Requests as usize].y = 0.0;
}
Some(Focus::Custom) => {
if self.selection {
self.custom.clear();
self.selection = false;
}
self.custom.extend(
value
.chars()
.filter(|c| c.is_ascii_hexdigit() || *c == '#')
.take(7),
);
}
Some(Focus::Source) | None => {}
}
}
pub(crate) fn reveal_cursor(&mut self) {
self.reveal_input = true;
}
pub fn ime_position(&self, layout: &DeveloperToolsLayout) -> Point {
if let Some(input) = layout.input {
let before = &self.input[..self.cursor];
let row = before.matches('\n').count() as f32;
let col = before.rsplit('\n').next().unwrap_or("");
let area = input.viewport;
return Point::new(
(input.origin.x + text::measure(col, TextStyle::code()).width)
.clamp(area.x, area.x + area.width),
(input.origin.y + row * ROW).clamp(area.y, area.y + area.height),
);
}
Point::new(layout.panel.x + 20.0, 60.0)
}
pub fn selected_text(&self) -> String {
if self.focus == Some(Focus::Source) {
return self.source_selection.text(self.selection);
}
if !self.selection {
return String::new();
}
match self.focus {
Some(Focus::Input) => self.input.clone(),
Some(Focus::Filter) => self.filter.clone(),
Some(Focus::Custom) => self.custom.clone(),
Some(Focus::Source) | None => String::new(),
}
}
pub fn key(&mut self, key: &winit::keyboard::NamedKey, shift: bool, command: bool) {
use winit::keyboard::NamedKey::*;
if *key == Escape {
self.focus = None;
self.preedit.clear();
self.interaction.blur();
return;
}
if self.focus == Some(Focus::Source) {
if let Some(row) = self.source_selection.navigate(key, shift, command) {
self.selection = false;
self.scroll[Region::Source as usize].y = (row as f32 * ROW - ROW * 4.0).max(0.0);
self.repaint = true;
}
return;
}
if self.focus == Some(Focus::Custom) && *key == Enter {
let hex = self.custom.trim_start_matches('#');
if hex.len() == 6
&& let Ok(v) = u32::from_str_radix(hex, 16)
{
self.accent = readable_accent(Color::rgb((v >> 16) as u8, (v >> 8) as u8, v as u8));
self.input_error.clear();
} else {
self.input_error = "Use a six-digit hex color, e.g. #1671D5".into();
}
return;
}
if self.focus != Some(Focus::Input) {
if *key == Backspace {
match self.focus {
Some(Focus::Filter) => {
if self.selection {
self.filter.clear();
} else {
self.filter.pop();
}
}
Some(Focus::Custom) => {
if self.selection {
self.custom.clear();
} else {
self.custom.pop();
}
}
_ => {}
}
self.selection = false;
}
return;
}
match key {
Enter if !shift => self.submit(),
Enter => self.insert("\n"),
Tab => self.insert(" "),
Space => self.insert(" "),
Backspace | Delete => {
if self.selection {
self.input.clear();
self.cursor = 0;
self.selection = false;
} else if *key == Backspace && self.cursor > 0 {
let n = self.input[..self.cursor]
.grapheme_indices(true)
.next_back()
.map_or(0, |(i, _)| i);
self.input.replace_range(n..self.cursor, "");
self.cursor = n;
} else if *key == Delete && self.cursor < self.input.len() {
let len = self.input[self.cursor..]
.graphemes(true)
.next()
.unwrap()
.len();
self.input.replace_range(self.cursor..self.cursor + len, "");
}
}
ArrowLeft => {
self.cursor = if command {
0
} else {
self.input[..self.cursor]
.grapheme_indices(true)
.next_back()
.map_or(0, |(i, _)| i)
};
self.selection = false;
}
ArrowRight => {
self.cursor = if command {
self.input.len()
} else {
self.cursor
+ self.input[self.cursor..]
.graphemes(true)
.next()
.map_or(0, str::len)
};
self.selection = false;
}
Home => {
self.cursor = if command {
0
} else {
self.input[..self.cursor].rfind('\n').map_or(0, |i| i + 1)
};
self.selection = false;
}
End => {
self.cursor = if command {
self.input.len()
} else {
self.input[self.cursor..]
.find('\n')
.map_or(self.input.len(), |i| self.cursor + i)
};
self.selection = false;
}
ArrowUp | ArrowDown if command || !self.input.contains('\n') => {
if self.history.is_empty() {
return;
}
if self.history_index.is_none() {
self.draft = self.input.clone();
}
let current = self.history_index.unwrap_or(self.history.len());
let index = if *key == ArrowUp {
current.saturating_sub(1)
} else {
(current + 1).min(self.history.len())
};
self.history_index = (index < self.history.len()).then_some(index);
self.input = self
.history_index
.map_or_else(|| self.draft.clone(), |i| self.history[i].clone());
self.cursor = self.input.len();
}
ArrowUp | ArrowDown => {
let start = self.input[..self.cursor].rfind('\n').map_or(0, |i| i + 1);
let col = self.input[start..self.cursor].graphemes(true).count();
let target = if *key == ArrowUp {
if start == 0 {
0
} else {
self.input[..start - 1].rfind('\n').map_or(0, |i| i + 1)
}
} else {
self.input[self.cursor..]
.find('\n')
.map_or(self.input.len(), |i| self.cursor + i + 1)
};
let end = self.input[target..]
.find('\n')
.map_or(self.input.len(), |i| target + i);
self.cursor = target
+ self.input[target..end]
.graphemes(true)
.take(col)
.map(str::len)
.sum::<usize>();
self.selection = false;
}
Escape => {
self.focus = None;
self.preedit.clear();
}
_ => {}
}
self.reveal_cursor();
}
pub fn initialize_tools(&mut self, name: &str) {
let name = name.to_owned();
let proxy = self.proxy.clone();
thread::spawn(move || {
let source = format!("ui.developerTools({name:?})");
let send = || -> std::io::Result<()> {
let mut stream = TcpStream::connect_timeout(
&"127.0.0.1:4737".parse().unwrap(),
Duration::from_secs(2),
)?;
stream.set_read_timeout(Some(Duration::from_secs(30)))?;
write!(
stream,
"POST /v1/namespaces/app.main/repl HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: {}\r\n\r\n{}",
source.len(),
source
)?;
let mut response = String::new();
stream.read_to_string(&mut response)?;
Ok(())
};
if let Err(error) = send()
&& let Some(proxy) = proxy
{
let _ = proxy.send_event(AppEvent::ConsoleFailed {
message: error.to_string(),
source: String::new(),
});
}
});
}
fn submit(&mut self) {
self.submit_console(false);
}
fn submit_console(&mut self, reset: bool) {
if !reset && self.input.trim().is_empty() {
return;
}
let source = if reset {
String::new()
} else {
self.input.clone()
};
let proxy = self.proxy.clone();
let spawned=thread::Builder::new().name("tessera-console-client".into()).spawn(move||{
let send=||->std::io::Result<()> {
let mut stream=TcpStream::connect_timeout(&"127.0.0.1:4737".parse().unwrap(),Duration::from_secs(2))?;
stream.set_read_timeout(Some(Duration::from_secs(120)))?;stream.set_write_timeout(Some(Duration::from_secs(10)))?;
let method=if reset{"DELETE"}else{"POST"};
write!(stream,"{method} /v1/namespaces/app.main/repl HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\nContent-Type: text/plain; charset=utf-8\r\nX-Tessera-Origin: console\r\nContent-Length: {}\r\n\r\n{}",source.len(),source)?;
let mut response=String::new();stream.read_to_string(&mut response)?;Ok(())
};
if let Err(error)=send()&&let Some(proxy)=proxy{let _=proxy.send_event(AppEvent::ConsoleFailed{message:error.to_string(),source});}
});
match spawned {
Ok(_) => {
if !reset {
self.history.push(self.input.clone());
if self.history.len() > 80 {
self.history.remove(0);
}
self.input.clear();
self.cursor = 0;
self.history_index = None;
self.selection = false;
self.reveal_cursor();
}
self.follow = true;
self.input_error.clear();
}
Err(error) => self.input_error = error.to_string(),
}
}
}
pub fn build(
width: u32,
height: u32,
scale: f32,
doc: &RenderDocument,
state: &mut DeveloperToolsState,
activity: &Activity,
) -> (DisplayList, DeveloperToolsLayout) {
if !state.open {
return (Default::default(), Default::default());
}
let width = width as f32 / scale;
let height = height as f32 / scale;
let w = width;
let x = 0.0;
let panel = Rect::new(x, 0.0, w, height);
let mut p = Painter {
ui: Ui::new(Theme::new(state.accent), state.interaction.clone()),
layout: DeveloperToolsLayout {
panel,
..Default::default()
},
};
p.fill(panel, BG);
p.ui.list.push(DrawCommand::PushClip(panel));
p.fill(Rect::new(x, 0.0, w, 31.0), BAR);
p.label(
12.0,
8.0,
match state.tab {
Tab::Console => "Console · app.main",
Tab::Sources => "Sources · read only",
Tab::Network => "Network · captured requests",
Tab::Settings => "Settings · appearance",
},
MUTED,
false,
);
let records = activity
.lock()
.unwrap()
.records
.iter()
.cloned()
.collect::<Vec<_>>();
let bottom = height - 22.0;
match state.tab {
Tab::Console => console::build(&mut p, state, &records, panel),
Tab::Sources => sources::build(&mut p, state, doc, panel),
Tab::Network => network::build(&mut p, state, &records, panel),
Tab::Settings => settings::build(&mut p, state, panel),
}
p.fill(Rect::new(x + 1.0, bottom, w - 1.0, 22.0), BAR);
p.label(
x + 9.0,
bottom + 3.0,
&text::fit(
&match state.tab {
Tab::Console => "Live session · Enter to run".into(),
Tab::Sources => "Read only · drag to select · ⌘/Ctrl+C to copy".into(),
Tab::Network => format!(
"{} captured requests",
records
.iter()
.filter(|r| r.id > state.network_after)
.count()
),
Tab::Settings => "Appearance · current window session".into(),
},
w - 20.0,
TextStyle::code(),
),
MUTED,
false,
);
// Splitters must be above toolbars, pane backgrounds and status bars for their
// whole length. Register them last so their hit priority matches their paint order.
for (rect, kind) in p.layout.splits.clone().into_iter().rev() {
p.ui.splitter(
rect,
WidgetId::new(("split", kind)),
match kind {
Split::Tree => Axis::Horizontal,
Split::Network => Axis::Vertical,
},
);
}
p.ui.list.push(DrawCommand::PopClip);
let mut scaled = DisplayList::default();
for cmd in p.ui.list.commands() {
let rect =
|r: &Rect| Rect::new(r.x * scale, r.y * scale, r.width * scale, r.height * scale);
scaled.push(match cmd {
DrawCommand::FillRoundedRect {
rect: r,
radius,
color,
} => DrawCommand::FillRoundedRect {
rect: rect(r),
radius: radius * scale,
color: *color,
},
DrawCommand::Text {
origin,
value,
style,
color,
} => DrawCommand::Text {
origin: Point::new(origin.x * scale, origin.y * scale),
value: value.clone(),
style: style.scaled(scale),
color: *color,
},
DrawCommand::PushClip(r) => DrawCommand::PushClip(rect(r)),
DrawCommand::StrokeRoundedRect {
rect: r,
radius,
width,
color,
} => DrawCommand::StrokeRoundedRect {
rect: rect(r),
radius: radius * scale,
width: width * scale,
color: *color,
},
other => other.clone(),
});
}
p.layout.widgets = p.ui.targets;
state.repaint = state.interaction.reconcile(&p.layout.widgets);
(scaled, p.layout)
}