undo/redo stack + find/replace bar with Cmd+F/G shortcuts

This commit is contained in:
jess 2026-04-08 03:12:52 -07:00
parent 01f34a4f34
commit 57488a2861
3 changed files with 513 additions and 39 deletions

View File

@ -159,7 +159,7 @@ class IcedViewportView: NSView {
if cmd && !shift { if cmd && !shift {
switch chars { switch chars {
case "a", "b", "c", "i", "v", "x", "z", "p", "t", case "a", "b", "c", "e", "f", "g", "i", "v", "x", "z", "p", "t",
"=", "+", "-", "0": "=", "+", "-", "0":
keyDown(with: event) keyDown(with: event)
return true return true
@ -168,7 +168,7 @@ class IcedViewportView: NSView {
} }
if cmd && shift { if cmd && shift {
switch chars { switch chars {
case "z": case "g", "z":
keyDown(with: event) keyDown(with: event)
return true return true
default: break default: break
@ -214,4 +214,16 @@ class IcedViewportView: NSView {
viewport_free_string(cstr) viewport_free_string(cstr)
return result return result
} }
func sendCommand(_ command: UInt32) {
guard let h = viewportHandle else { return }
viewport_send_command(h, command)
}
func setTheme(_ name: String) {
guard let h = viewportHandle else { return }
name.withCString { cstr in
viewport_set_theme(h, cstr)
}
}
} }

View File

@ -1,4 +1,5 @@
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant;
use iced_wgpu::core::keyboard; use iced_wgpu::core::keyboard;
use iced_wgpu::core::keyboard::key; use iced_wgpu::core::keyboard::key;
@ -11,7 +12,10 @@ use iced_widget::canvas;
use iced_widget::container; use iced_widget::container;
use iced_widget::markdown; use iced_widget::markdown;
use iced_widget::text_editor::{self, Action, Binding, Cursor, KeyPress, Motion, Position, Status, Style}; use iced_widget::text_editor::{self, Action, Binding, Cursor, KeyPress, Motion, Position, Status, Style};
use iced_widget::text_input;
use iced_wgpu::core::text::highlighter::Format; use iced_wgpu::core::text::highlighter::Format;
use iced_wgpu::core::widget::Id as WidgetId;
use crate::palette;
use crate::syntax::{self, SyntaxHighlighter, SyntaxSettings}; use crate::syntax::{self, SyntaxHighlighter, SyntaxSettings};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -28,11 +32,62 @@ pub enum Message {
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
ZoomReset, ZoomReset,
Undo,
Redo,
ToggleFind,
HideFind,
FindQueryChanged(String),
FindNext,
FindPrev,
ReplaceQueryChanged(String),
ReplaceOne,
ReplaceAll,
} }
pub const RESULT_PREFIX: &str = ""; pub const RESULT_PREFIX: &str = "";
pub const ERROR_PREFIX: &str = ""; pub const ERROR_PREFIX: &str = "";
pub const FIND_INPUT_ID: &str = "find_input";
pub const REPLACE_INPUT_ID: &str = "replace_input";
const UNDO_MAX: usize = 200;
const COALESCE_MS: u128 = 500;
struct UndoSnapshot {
text: String,
cursor_line: usize,
cursor_col: usize,
}
#[derive(PartialEq, Eq, Clone, Copy)]
enum EditKind {
Insert,
Backspace,
Delete,
Enter,
Paste,
Other,
}
pub struct FindState {
pub visible: bool,
pub query: String,
pub replacement: String,
pub matches: Vec<(usize, usize)>,
pub current: usize,
}
impl FindState {
fn new() -> Self {
Self {
visible: false,
query: String::new(),
replacement: String::new(),
matches: Vec::new(),
current: 0,
}
}
}
pub struct EditorState { pub struct EditorState {
pub content: text_editor::Content<iced_wgpu::Renderer>, pub content: text_editor::Content<iced_wgpu::Renderer>,
pub font_size: f32, pub font_size: f32,
@ -40,20 +95,29 @@ pub struct EditorState {
pub parsed: Vec<markdown::Item>, pub parsed: Vec<markdown::Item>,
pub lang: Option<String>, pub lang: Option<String>,
scroll_offset: f32, scroll_offset: f32,
undo_stack: Vec<UndoSnapshot>,
redo_stack: Vec<UndoSnapshot>,
last_edit_kind: EditKind,
last_edit_time: Instant,
pub find: FindState,
pub pending_focus: Option<WidgetId>,
} }
fn md_style() -> markdown::Style { fn md_style() -> markdown::Style {
let p = palette::current();
markdown::Style { markdown::Style {
font: Font::default(), font: Font::default(),
inline_code_highlight: Highlight { inline_code_highlight: Highlight {
background: Color::from_rgb(0.188, 0.188, 0.259).into(), background: p.surface0.into(),
border: border::rounded(4), border: border::rounded(4),
}, },
inline_code_padding: padding::left(2).right(2), inline_code_padding: padding::left(2).right(2),
inline_code_color: Color::from_rgb(0.651, 0.890, 0.631), inline_code_color: p.green,
inline_code_font: Font::MONOSPACE, inline_code_font: Font::MONOSPACE,
code_block_font: Font::MONOSPACE, code_block_font: Font::MONOSPACE,
link_color: Color::from_rgb(0.537, 0.706, 0.980), link_color: p.blue,
} }
} }
@ -91,6 +155,12 @@ impl EditorState {
parsed: Vec::new(), parsed: Vec::new(),
lang: Some("rust".into()), lang: Some("rust".into()),
scroll_offset: 0.0, scroll_offset: 0.0,
undo_stack: Vec::new(),
redo_stack: Vec::new(),
last_edit_kind: EditKind::Other,
last_edit_time: Instant::now(),
find: FindState::new(),
pending_focus: None,
} }
} }
@ -186,11 +256,144 @@ impl EditorState {
}); });
} }
pub fn take_pending_focus(&mut self) -> Option<WidgetId> {
self.pending_focus.take()
}
fn snapshot(&self) -> UndoSnapshot {
let cursor = self.content.cursor();
UndoSnapshot {
text: self.get_clean_text(),
cursor_line: cursor.position.line,
cursor_col: cursor.position.column,
}
}
fn push_undo_snapshot(&mut self) {
let snap = self.snapshot();
self.undo_stack.push(snap);
if self.undo_stack.len() > UNDO_MAX {
self.undo_stack.remove(0);
}
}
fn maybe_snapshot(&mut self, kind: EditKind) {
let now = Instant::now();
let elapsed = now.duration_since(self.last_edit_time).as_millis();
let should_snap = kind != self.last_edit_kind
|| elapsed > COALESCE_MS
|| kind == EditKind::Enter
|| kind == EditKind::Paste;
if should_snap {
self.push_undo_snapshot();
}
self.last_edit_kind = kind;
self.last_edit_time = now;
self.redo_stack.clear();
}
fn classify_edit(action: &text_editor::Action) -> Option<EditKind> {
match action {
Action::Edit(edit) => match edit {
text_editor::Edit::Insert(_) => Some(EditKind::Insert),
text_editor::Edit::Enter => Some(EditKind::Enter),
text_editor::Edit::Backspace => Some(EditKind::Backspace),
text_editor::Edit::Delete => Some(EditKind::Delete),
text_editor::Edit::Paste(_) => Some(EditKind::Paste),
_ => Some(EditKind::Other),
},
_ => None,
}
}
fn restore_snapshot(&mut self, snap: &UndoSnapshot) {
self.set_text(&snap.text);
self.run_eval();
let text = self.content.text();
let display_line = from_clean_line(&text, snap.cursor_line);
self.content.move_to(Cursor {
position: Position { line: display_line, column: snap.cursor_col },
selection: None,
});
}
fn perform_undo(&mut self) {
if self.undo_stack.is_empty() {
return;
}
let current = self.snapshot();
self.redo_stack.push(current);
let snap = self.undo_stack.pop().unwrap();
self.restore_snapshot(&snap);
self.last_edit_kind = EditKind::Other;
}
fn perform_redo(&mut self) {
if self.redo_stack.is_empty() {
return;
}
let current = self.snapshot();
self.undo_stack.push(current);
let snap = self.redo_stack.pop().unwrap();
self.restore_snapshot(&snap);
self.last_edit_kind = EditKind::Other;
}
fn update_find_matches(&mut self) {
self.find.matches.clear();
self.find.current = 0;
if self.find.query.is_empty() {
return;
}
let text = self.get_clean_text();
let query_lower = self.find.query.to_lowercase();
let text_lower = text.to_lowercase();
let mut line = 0usize;
let mut col = 0usize;
let mut byte = 0usize;
for (i, ch) in text_lower.char_indices() {
while byte < i {
byte += 1;
}
if ch == '\n' {
line += 1;
col = 0;
continue;
}
if text_lower[i..].starts_with(&query_lower) {
self.find.matches.push((line, col));
}
col += 1;
}
}
fn navigate_to_match(&mut self) {
if self.find.matches.is_empty() {
return;
}
let idx = self.find.current.min(self.find.matches.len() - 1);
let (line, col) = self.find.matches[idx];
let text = self.content.text();
let display_line = from_clean_line(&text, line);
self.content.move_to(Cursor {
position: Position { line: display_line, column: col },
selection: None,
});
}
pub fn update(&mut self, message: Message) { pub fn update(&mut self, message: Message) {
match message { match message {
Message::EditorAction(action) => { Message::EditorAction(action) => {
let is_edit = action.is_edit(); let is_edit = action.is_edit();
if let Some(kind) = Self::classify_edit(&action) {
self.maybe_snapshot(kind);
}
if let Action::Scroll { lines } = &action { if let Action::Scroll { lines } = &action {
let lh = self.line_height(); let lh = self.line_height();
self.scroll_offset += *lines as f32 * lh; self.scroll_offset += *lines as f32 * lh;
@ -312,6 +515,107 @@ impl EditorState {
Message::ZoomReset => { Message::ZoomReset => {
self.font_size = 14.0; self.font_size = 14.0;
} }
Message::Undo => {
self.perform_undo();
}
Message::Redo => {
self.perform_redo();
}
Message::ToggleFind => {
self.find.visible = !self.find.visible;
if self.find.visible {
self.pending_focus = Some(WidgetId::new(FIND_INPUT_ID));
}
}
Message::HideFind => {
self.find.visible = false;
}
Message::FindQueryChanged(q) => {
self.find.query = q;
self.update_find_matches();
if !self.find.matches.is_empty() {
self.find.current = 0;
self.navigate_to_match();
}
}
Message::FindNext => {
if !self.find.matches.is_empty() {
self.find.current = (self.find.current + 1) % self.find.matches.len();
self.navigate_to_match();
}
}
Message::FindPrev => {
if !self.find.matches.is_empty() {
self.find.current = if self.find.current == 0 {
self.find.matches.len() - 1
} else {
self.find.current - 1
};
self.navigate_to_match();
}
}
Message::ReplaceQueryChanged(r) => {
self.find.replacement = r;
}
Message::ReplaceOne => {
if self.find.matches.is_empty() || self.find.query.is_empty() {
return;
}
self.push_undo_snapshot();
self.redo_stack.clear();
let (match_line, match_col) = self.find.matches[self.find.current];
let clean = self.get_clean_text();
let mut lines: Vec<String> = clean.lines().map(|l| l.to_string()).collect();
if match_line < lines.len() {
let line = &lines[match_line];
let query_len = self.find.query.len();
let line_lower = line.to_lowercase();
let query_lower = self.find.query.to_lowercase();
if let Some(byte_start) = nth_char_byte_offset(line, match_col) {
if line_lower[byte_start..].starts_with(&query_lower) {
let before = &line[..byte_start];
let after = &line[byte_start + query_len..];
lines[match_line] = format!("{before}{}{after}", self.find.replacement);
}
}
}
let new_text = lines.join("\n");
self.set_text(&new_text);
self.run_eval();
self.update_find_matches();
if !self.find.matches.is_empty() {
self.find.current = self.find.current.min(self.find.matches.len() - 1);
self.navigate_to_match();
}
}
Message::ReplaceAll => {
if self.find.matches.is_empty() || self.find.query.is_empty() {
return;
}
self.push_undo_snapshot();
self.redo_stack.clear();
let clean = self.get_clean_text();
let query_lower = self.find.query.to_lowercase();
let mut result = String::with_capacity(clean.len());
let mut i = 0;
let clean_lower = clean.to_lowercase();
let qlen = self.find.query.len();
while i < clean.len() {
if clean_lower[i..].starts_with(&query_lower) {
result.push_str(&self.find.replacement);
i += qlen;
} else {
let ch = clean[i..].chars().next().unwrap();
result.push(ch);
i += ch.len_utf8();
}
}
self.set_text(&result);
self.run_eval();
self.update_find_matches();
}
} }
} }
@ -330,12 +634,15 @@ impl EditorState {
) )
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.style(|_theme: &Theme| container::Style { .style(|_theme: &Theme| {
background: Some(Background::Color(Color::from_rgb(0.08, 0.08, 0.10))), let p = palette::current();
container::Style {
background: Some(Background::Color(p.base)),
border: Border::default(), border: Border::default(),
text_color: Some(Color::from_rgb(0.804, 0.839, 0.957)), text_color: Some(p.text),
shadow: Shadow::default(), shadow: Shadow::default(),
snap: false, snap: false,
}
}) })
.into() .into()
} else { } else {
@ -348,12 +655,15 @@ impl EditorState {
.padding(Padding { top: top_pad, right: 8.0, bottom: 8.0, left: 8.0 }) .padding(Padding { top: top_pad, right: 8.0, bottom: 8.0, left: 8.0 })
.wrapping(Wrapping::Word) .wrapping(Wrapping::Word)
.key_binding(macos_key_binding) .key_binding(macos_key_binding)
.style(|_theme, _status| Style { .style(|_theme, _status| {
background: Background::Color(Color::from_rgb(0.08, 0.08, 0.10)), let p = palette::current();
Style {
background: Background::Color(p.base),
border: Border::default(), border: Border::default(),
placeholder: Color::from_rgb(0.4, 0.4, 0.4), placeholder: p.overlay0,
value: Color::WHITE, value: p.text,
selection: Color::from_rgba(0.3, 0.5, 0.8, 0.4), selection: Color { a: 0.4, ..p.blue },
}
}); });
let settings = SyntaxSettings { let settings = SyntaxSettings {
@ -365,7 +675,7 @@ impl EditorState {
settings, settings,
|highlight, _theme| Format { |highlight, _theme| Format {
color: Some(syntax::highlight_color(highlight.kind)), color: Some(syntax::highlight_color(highlight.kind)),
font: None, font: syntax::highlight_font(highlight.kind),
}, },
) )
.into(); .into();
@ -401,29 +711,149 @@ impl EditorState {
iced_widget::text(format!("{mode_label} Ln {line}, Col {col}")) iced_widget::text(format!("{mode_label} Ln {line}, Col {col}"))
.font(Font::MONOSPACE) .font(Font::MONOSPACE)
.size(11.0) .size(11.0)
.color(Color::from_rgb(0.55, 0.55, 0.55)) .color(palette::current().overlay1)
.into(), .into(),
]) ])
) )
.width(Length::Fill) .width(Length::Fill)
.padding(Padding { top: 3.0, right: 10.0, bottom: 3.0, left: 10.0 }) .padding(Padding { top: 3.0, right: 10.0, bottom: 3.0, left: 10.0 })
.style(|_theme: &Theme| container::Style { .style(|_theme: &Theme| {
background: Some(Background::Color(Color::from_rgb(0.12, 0.12, 0.14))), let p = palette::current();
container::Style {
background: Some(Background::Color(p.mantle)),
border: Border::default(), border: Border::default(),
text_color: None, text_color: None,
shadow: Shadow::default(), shadow: Shadow::default(),
snap: false, snap: false,
}
}); });
let mut col_items: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> = let mut col_items: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> = Vec::new();
vec![main_content];
if self.find.visible {
col_items.push(self.find_bar());
}
col_items.push(main_content);
col_items.push(status_bar.into()); col_items.push(status_bar.into());
iced_widget::column(col_items) iced_widget::column(col_items)
.height(Length::Fill) .height(Length::Fill)
.into() .into()
} }
fn find_bar(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
let p = palette::current();
let search_input = text_input::TextInput::new("Find...", &self.find.query)
.on_input(Message::FindQueryChanged)
.on_submit(Message::FindNext)
.id(WidgetId::new(FIND_INPUT_ID))
.font(Font::MONOSPACE)
.size(13.0)
.padding(Padding { top: 3.0, right: 6.0, bottom: 3.0, left: 6.0 })
.width(Length::FillPortion(3))
.style(find_input_style);
let replace_input = text_input::TextInput::new("Replace...", &self.find.replacement)
.on_input(Message::ReplaceQueryChanged)
.on_submit(Message::ReplaceOne)
.id(WidgetId::new(REPLACE_INPUT_ID))
.font(Font::MONOSPACE)
.size(13.0)
.padding(Padding { top: 3.0, right: 6.0, bottom: 3.0, left: 6.0 })
.width(Length::FillPortion(3))
.style(find_input_style);
let match_label = if self.find.matches.is_empty() {
if self.find.query.is_empty() {
String::new()
} else {
"0/0".into()
}
} else {
format!("{}/{}", self.find.current + 1, self.find.matches.len())
};
let label: Element<'_, Message, Theme, iced_wgpu::Renderer> =
iced_widget::text(match_label)
.font(Font::MONOSPACE)
.size(11.0)
.color(p.overlay1)
.into();
let btn = |txt: String, msg: Message| -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
iced_widget::button(
iced_widget::text(txt).font(Font::MONOSPACE).size(11.0)
)
.on_press(msg)
.padding(Padding { top: 2.0, right: 6.0, bottom: 2.0, left: 6.0 })
.style(find_btn_style)
.into()
};
let row = iced_widget::row![
search_input,
label,
btn("Prev".into(), Message::FindPrev),
btn("Next".into(), Message::FindNext),
replace_input,
btn("Repl".into(), Message::ReplaceOne),
btn("All".into(), Message::ReplaceAll),
btn("X".into(), Message::HideFind),
]
.spacing(4.0)
.align_y(alignment::Vertical::Center);
iced_widget::container(row)
.width(Length::Fill)
.padding(Padding { top: 4.0, right: 8.0, bottom: 4.0, left: 8.0 })
.style(|_theme: &Theme| {
let p = palette::current();
container::Style {
background: Some(Background::Color(p.mantle)),
border: Border::default(),
text_color: None,
shadow: Shadow::default(),
snap: false,
}
})
.into()
}
}
fn find_input_style(_theme: &Theme, _status: text_input::Status) -> text_input::Style {
let p = palette::current();
text_input::Style {
background: Background::Color(p.surface0),
border: Border {
color: p.surface2,
width: 1.0,
radius: 3.0.into(),
},
icon: p.overlay2,
placeholder: p.overlay0,
value: p.text,
selection: Color { a: 0.4, ..p.blue },
}
}
fn find_btn_style(
_theme: &Theme,
_status: iced_widget::button::Status,
) -> iced_widget::button::Style {
let p = palette::current();
iced_widget::button::Style {
background: Some(Background::Color(p.surface1)),
text_color: p.text,
border: Border {
color: p.surface2,
width: 1.0,
radius: 3.0.into(),
},
shadow: Shadow::default(),
snap: false,
}
} }
struct Gutter { struct Gutter {
@ -463,7 +893,7 @@ impl canvas::Program<Message, Theme, iced_wgpu::Renderer> for Gutter {
frame.fill_rectangle( frame.fill_rectangle(
Point::ORIGIN, Point::ORIGIN,
bounds.size(), bounds.size(),
Color::from_rgb(0.06, 0.06, 0.08), palette::current().crust,
); );
let first_visible = (self.scroll_offset / lh).floor() as usize; let first_visible = (self.scroll_offset / lh).floor() as usize;
@ -481,10 +911,11 @@ impl canvas::Program<Message, Theme, iced_wgpu::Renderer> for Gutter {
if y + lh < 0.0 || y > bounds.height { if y + lh < 0.0 || y > bounds.height {
continue; continue;
} }
let p = palette::current();
let color = if line_idx == self.cursor_line { let color = if line_idx == self.cursor_line {
Color::from_rgb(0.55, 0.55, 0.62) p.overlay1
} else { } else {
Color::from_rgb(0.35, 0.35, 0.42) p.surface2
}; };
frame.fill_text(canvas::Text { frame.fill_text(canvas::Text {
content: format!("{}", line_idx + 1), content: format!("{}", line_idx + 1),
@ -645,6 +1076,12 @@ fn macos_key_binding(key_press: KeyPress) -> Option<Binding<Message>> {
} }
match key.as_ref() { match key.as_ref() {
keyboard::Key::Character("z") if modifiers.logo() && modifiers.shift() => {
Some(Binding::Custom(Message::Redo))
}
keyboard::Key::Character("z") if modifiers.logo() => {
Some(Binding::Custom(Message::Undo))
}
keyboard::Key::Character("=" | "+") if modifiers.logo() => { keyboard::Key::Character("=" | "+") if modifiers.logo() => {
Some(Binding::Custom(Message::ZoomIn)) Some(Binding::Custom(Message::ZoomIn))
} }
@ -736,3 +1173,7 @@ fn leading_whitespace(line: &str) -> &str {
let end = line.len() - line.trim_start().len(); let end = line.len() - line.trim_start().len();
&line[..end] &line[..end]
} }
fn nth_char_byte_offset(s: &str, char_idx: usize) -> Option<usize> {
s.char_indices().nth(char_idx).map(|(i, _)| i)
}

View File

@ -12,6 +12,7 @@ use raw_window_handle::{
}; };
use crate::editor::{EditorState, Message}; use crate::editor::{EditorState, Message};
use crate::palette;
use crate::ViewportHandle; use crate::ViewportHandle;
struct MacClipboard; struct MacClipboard;
@ -158,22 +159,35 @@ pub fn render(handle: &mut ViewportHandle) {
let mut messages: Vec<Message> = Vec::new(); let mut messages: Vec<Message> = Vec::new();
for event in &handle.events { for event in &handle.events {
if let Event::Keyboard(keyboard::Event::KeyPressed { match event {
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Character(c), key: keyboard::Key::Character(c),
modifiers, modifiers,
.. ..
}) = event }) if modifiers.logo() => {
{
if modifiers.logo() {
match c.as_str() { match c.as_str() {
"p" => messages.push(Message::TogglePreview), "p" => messages.push(Message::TogglePreview),
"t" => messages.push(Message::InsertTable), "t" => messages.push(Message::InsertTable),
"b" => messages.push(Message::ToggleBold), "b" => messages.push(Message::ToggleBold),
"i" => messages.push(Message::ToggleItalic), "i" => messages.push(Message::ToggleItalic),
"e" => messages.push(Message::SmartEval), "e" => messages.push(Message::SmartEval),
"z" if modifiers.shift() => messages.push(Message::Redo),
"z" => messages.push(Message::Undo),
"f" => messages.push(Message::ToggleFind),
"g" if modifiers.shift() => messages.push(Message::FindPrev),
"g" => messages.push(Message::FindNext),
_ => {} _ => {}
} }
} }
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::Escape),
..
}) => {
if handle.state.find.visible {
messages.push(Message::HideFind);
}
}
_ => {}
} }
} }
@ -192,6 +206,8 @@ pub fn render(handle: &mut ViewportHandle) {
handle.state.update(msg); handle.state.update(msg);
} }
let pending_focus = handle.state.take_pending_focus();
let theme = Theme::Dark; let theme = Theme::Dark;
let style = Style { let style = Style {
text_color: Color::WHITE, text_color: Color::WHITE,
@ -204,13 +220,18 @@ pub fn render(handle: &mut ViewportHandle) {
&mut handle.renderer, &mut handle.renderer,
); );
if let Some(focus_id) = pending_focus {
use iced_wgpu::core::widget::operation::focusable;
let mut op = focusable::focus(focus_id);
ui.operate(&handle.renderer, &mut op);
}
ui.draw(&mut handle.renderer, &theme, &style, handle.cursor); ui.draw(&mut handle.renderer, &theme, &style, handle.cursor);
handle.cache = ui.into_cache(); handle.cache = ui.into_cache();
let bg = Color::from_rgb(0.08, 0.08, 0.10);
handle handle
.renderer .renderer
.present(Some(bg), handle.format, &view, &handle.viewport); .present(Some(palette::current().base), handle.format, &view, &handle.viewport);
frame.present(); frame.present();
} }