Add retained paints and shared text selection
This commit is contained in:
1 parent
25370731d0
commit
de92fccba5
60 files changed
+2812
-1261
No files matched your search
@@ -88,7 +88,7 @@ impl DefaultAppState for Client {
|
|||||||
}
|
}
|
||||||
Err(message) => {
|
Err(message) => {
|
||||||
let text = wtext(format!("Couldn't fold the bench fixture: {message}"))
|
let text = wtext(format!("Couldn't fold the bench fixture: {message}"))
|
||||||
.color(Color::WHITE)
|
.color(PaintId::WHITE)
|
||||||
.wrap(true)
|
.wrap(true)
|
||||||
.pad(dp(16))
|
.pad(dp(16))
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ impl HasAndroidUiState for BenchClient {
|
|||||||
|
|
||||||
fn placeholder<Rsc: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget {
|
fn placeholder<Rsc: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget {
|
||||||
wtext(message.to_string())
|
wtext(message.to_string())
|
||||||
.color(Color::WHITE)
|
.color(PaintId::WHITE)
|
||||||
.wrap(true)
|
.wrap(true)
|
||||||
.pad(16)
|
.pad(16)
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
@@ -124,7 +124,7 @@ impl AndroidAppState for BenchClient {
|
|||||||
.text_align(Align::LEFT)
|
.text_align(Align::LEFT)
|
||||||
.wrap(true)
|
.wrap(true)
|
||||||
.size(14)
|
.size(14)
|
||||||
.color(Color::WHITE)
|
.color(PaintId::WHITE)
|
||||||
.attr::<Selectable>(())
|
.attr::<Selectable>(())
|
||||||
.label("Benchmark report")
|
.label("Benchmark report")
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
@@ -277,7 +277,7 @@ fn trace_line(at_start: bool, at_end: bool) -> String {
|
|||||||
/// why it needs one at all. A dark neutral rather than pure black
|
/// why it needs one at all. A dark neutral rather than pure black
|
||||||
/// (`android::render::CLEAR_COLOR`) so the row reads as a distinct panel
|
/// (`android::render::CLEAR_COLOR`) so the row reads as a distinct panel
|
||||||
/// instead of a hole in the background the buttons happen to float in.
|
/// instead of a hole in the background the buttons happen to float in.
|
||||||
const HEADER_SURFACE: UiColor = UiColor::new(28, 28, 34, 255);
|
const HEADER_SURFACE: Srgba8 = Srgba8::new(28, 28, 34, 255);
|
||||||
|
|
||||||
/// `top_pad` is the status-bar inset in physical pixels (0.0 until
|
/// `top_pad` is the status-bar inset in physical pixels (0.0 until
|
||||||
/// `on_insets_changed` has run once) -- folded in here, rather than
|
/// `on_insets_changed` has run once) -- folded in here, rather than
|
||||||
@@ -289,7 +289,7 @@ const HEADER_TEXT: f32 = 18.0;
|
|||||||
const HEADER_ROW_HEIGHT_DP: f32 = 56.0;
|
const HEADER_ROW_HEIGHT_DP: f32 = 56.0;
|
||||||
|
|
||||||
fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
|
fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
|
||||||
let run_rect = rect(Color::rgb(40, 70, 40))
|
let run_rect = rect(Srgba8::rgb(40, 70, 40))
|
||||||
.on(
|
.on(
|
||||||
CursorSense::click(),
|
CursorSense::click(),
|
||||||
|ctx: EventIdCtx<'_, Rsc, _, _>, rsc: &mut Rsc| {
|
|ctx: EventIdCtx<'_, Rsc, _, _>, rsc: &mut Rsc| {
|
||||||
@@ -307,7 +307,7 @@ fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
|
|||||||
.pad(dp(8))
|
.pad(dp(8))
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let copy_rect = rect(Color::rgb(50, 50, 60))
|
let copy_rect = rect(Srgba8::rgb(50, 50, 60))
|
||||||
.on(
|
.on(
|
||||||
CursorSense::click(),
|
CursorSense::click(),
|
||||||
|ctx: EventIdCtx<'_, Rsc, _, _>, rsc: &mut Rsc| {
|
|ctx: EventIdCtx<'_, Rsc, _, _>, rsc: &mut Rsc| {
|
||||||
@@ -325,7 +325,7 @@ fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
|
|||||||
.pad(dp(8))
|
.pad(dp(8))
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let diag_rect = rect(Color::rgb(60, 45, 70))
|
let diag_rect = rect(Srgba8::rgb(60, 45, 70))
|
||||||
.on(
|
.on(
|
||||||
CursorSense::click(),
|
CursorSense::click(),
|
||||||
|ctx: EventIdCtx<'_, Rsc, _, _>, rsc: &mut Rsc| {
|
|ctx: EventIdCtx<'_, Rsc, _, _>, rsc: &mut Rsc| {
|
||||||
@@ -351,9 +351,9 @@ fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
|
|||||||
// either full of trace or has none.
|
// either full of trace or has none.
|
||||||
let tracing = iris::diagnostics::trace_enabled();
|
let tracing = iris::diagnostics::trace_enabled();
|
||||||
let trace_rect = rect(if tracing {
|
let trace_rect = rect(if tracing {
|
||||||
Color::rgb(90, 70, 30)
|
Srgba8::rgb(90, 70, 30)
|
||||||
} else {
|
} else {
|
||||||
Color::rgb(50, 50, 60)
|
Srgba8::rgb(50, 50, 60)
|
||||||
})
|
})
|
||||||
.on(
|
.on(
|
||||||
CursorSense::click(),
|
CursorSense::click(),
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ fn build_transport() -> Result<UreqTransport, String> {
|
|||||||
|
|
||||||
fn placeholder<Rsc: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget {
|
fn placeholder<Rsc: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget {
|
||||||
wtext(message.to_string())
|
wtext(message.to_string())
|
||||||
.color(Color::WHITE)
|
.color(PaintId::WHITE)
|
||||||
.wrap(true)
|
.wrap(true)
|
||||||
.pad(16)
|
.pad(16)
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
@@ -45,7 +45,7 @@ fn placeholder<Rsc: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget {
|
|||||||
|
|
||||||
fn frame_report_controls(rsc: &mut AndroidRsc<TranscriptClient>) -> WeakWidget {
|
fn frame_report_controls(rsc: &mut AndroidRsc<TranscriptClient>) -> WeakWidget {
|
||||||
type Rsc = AndroidRsc<TranscriptClient>;
|
type Rsc = AndroidRsc<TranscriptClient>;
|
||||||
let report_rect = rect(Color::rgb(50, 50, 60))
|
let report_rect = rect(Srgba8::rgb(50, 50, 60))
|
||||||
.on(
|
.on(
|
||||||
CursorSense::click(),
|
CursorSense::click(),
|
||||||
|ctx: EventIdCtx<'_, Rsc, _, _>, _rsc: &mut Rsc| match ctx
|
|ctx: EventIdCtx<'_, Rsc, _, _>, _rsc: &mut Rsc| match ctx
|
||||||
@@ -69,7 +69,7 @@ fn frame_report_controls(rsc: &mut AndroidRsc<TranscriptClient>) -> WeakWidget {
|
|||||||
.pad(8)
|
.pad(8)
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let reset_rect = rect(Color::rgb(70, 40, 40))
|
let reset_rect = rect(Srgba8::rgb(70, 40, 40))
|
||||||
.on(
|
.on(
|
||||||
CursorSense::click(),
|
CursorSense::click(),
|
||||||
|ctx: EventIdCtx<'_, Rsc, _, _>, _rsc: &mut Rsc| {
|
|ctx: EventIdCtx<'_, Rsc, _, _>, _rsc: &mut Rsc| {
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ impl Client {
|
|||||||
list(rsc).push(row);
|
list(rsc).push(row);
|
||||||
}
|
}
|
||||||
let tree = list
|
let tree = list
|
||||||
.background(rect(Color::rgb(24, 24, 28)))
|
.background(rect(Srgba8::rgb(24, 24, 28)))
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any();
|
.any();
|
||||||
(self.list_ptr)(rsc).set(tree);
|
(self.list_ptr)(rsc).set(tree);
|
||||||
@@ -299,14 +299,14 @@ fn session_row(
|
|||||||
selected: bool,
|
selected: bool,
|
||||||
) -> StrongWidget {
|
) -> StrongWidget {
|
||||||
let bg = if selected {
|
let bg = if selected {
|
||||||
Color::rgb(58, 90, 138)
|
Srgba8::rgb(58, 90, 138)
|
||||||
} else {
|
} else {
|
||||||
Color::rgb(38, 38, 44)
|
Srgba8::rgb(38, 38, 44)
|
||||||
};
|
};
|
||||||
let id = session.id.clone();
|
let id = session.id.clone();
|
||||||
let label = format!("{}\n{}", session.title, session.status);
|
let label = format!("{}\n{}", session.title, session.status);
|
||||||
wtext(label)
|
wtext(label)
|
||||||
.color(Color::WHITE)
|
.color(PaintId::WHITE)
|
||||||
.wrap(true)
|
.wrap(true)
|
||||||
.pad(10)
|
.pad(10)
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
@@ -323,7 +323,7 @@ fn session_row(
|
|||||||
|
|
||||||
fn placeholder(rsc: &mut DefaultRsc<Client>, message: &str) -> StrongWidget {
|
fn placeholder(rsc: &mut DefaultRsc<Client>, message: &str) -> StrongWidget {
|
||||||
wtext(message.to_string())
|
wtext(message.to_string())
|
||||||
.color(Color::WHITE)
|
.color(PaintId::WHITE)
|
||||||
.wrap(true)
|
.wrap(true)
|
||||||
.pad(16)
|
.pad(16)
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
|
use crate::ui::theme::Theme;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
|
|
||||||
const MAX_LINES: f32 = 6.0;
|
const MAX_LINES: f32 = 6.0;
|
||||||
const APPROX_LINE_HEIGHT_DP: f32 = 24.0;
|
const APPROX_LINE_HEIGHT_DP: f32 = 24.0;
|
||||||
const FIELD_PAD_DP: f32 = 12.0;
|
const FIELD_PAD_DP: f32 = 12.0;
|
||||||
|
|
||||||
const BAR_FILL: UiColor = UiColor::new(40, 40, 46, 255);
|
|
||||||
|
|
||||||
/// `field` is exposed so the caller can read its content on submit
|
/// `field` is exposed so the caller can read its content on submit
|
||||||
/// (`field.edit(rsc).text()`) and clear it afterward
|
/// (`field.edit(rsc).text()`) and clear it afterward
|
||||||
/// (`field.edit(rsc).set("")`).
|
/// (`field.edit(rsc).set("")`).
|
||||||
@@ -43,7 +42,7 @@ impl Composer {
|
|||||||
/// panics ("was already added", `core/src/widget/like.rs:12`) -- the same
|
/// panics ("was already added", `core/src/widget/like.rs:12`) -- the same
|
||||||
/// mistake this box's `row.rs` first made with its sender-label header, see
|
/// mistake this box's `row.rs` first made with its sender-label header, see
|
||||||
/// that file's comment for the fuller account.
|
/// that file's comment for the fuller account.
|
||||||
pub fn build_composer<Rsc: HasEvents>(rsc: &mut Rsc) -> (Composer, WeakWidget)
|
pub fn build_composer<Rsc: HasEvents>(rsc: &mut Rsc, theme: &Theme) -> (Composer, WeakWidget)
|
||||||
where
|
where
|
||||||
Rsc::State: FocusHost,
|
Rsc::State: FocusHost,
|
||||||
{
|
{
|
||||||
@@ -52,7 +51,7 @@ where
|
|||||||
.text_align(Align::LEFT)
|
.text_align(Align::LEFT)
|
||||||
.wrap(true)
|
.wrap(true)
|
||||||
.size(18)
|
.size(18)
|
||||||
.color(UiColor::WHITE)
|
.color(theme.text.clone())
|
||||||
.attr::<Selectable>(())
|
.attr::<Selectable>(())
|
||||||
.label("Message")
|
.label("Message")
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
@@ -66,7 +65,7 @@ where
|
|||||||
.pad(dp(FIELD_PAD_DP))
|
.pad(dp(FIELD_PAD_DP))
|
||||||
.max_height(dp(APPROX_LINE_HEIGHT_DP * MAX_LINES + FIELD_PAD_DP * 2.0))
|
.max_height(dp(APPROX_LINE_HEIGHT_DP * MAX_LINES + FIELD_PAD_DP * 2.0))
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
.masked_by(rect(BAR_FILL))
|
.masked_by(rect(theme.composer_surface.clone()))
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let outer_pad: WeakWidget<Pad> = content.pad(Padding::ZERO).add(rsc);
|
let outer_pad: WeakWidget<Pad> = content.pad(Padding::ZERO).add(rsc);
|
||||||
|
|||||||
+66
-49
@@ -1,54 +1,36 @@
|
|||||||
use crate::client::highlight::{self, Kind, Language};
|
use crate::client::highlight::{self, Kind, Language};
|
||||||
use crate::client::markdown_blocks::{Block, BlockKind};
|
use crate::client::markdown_blocks::{Block, BlockKind};
|
||||||
|
use crate::ui::theme::Theme;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
|
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
const fn mocha(hex: u32) -> UiColor {
|
fn syntax_color(kind: Kind, theme: &Theme) -> PaintId {
|
||||||
UiColor::new(
|
|
||||||
((hex >> 16) & 0xff) as u8,
|
|
||||||
((hex >> 8) & 0xff) as u8,
|
|
||||||
(hex & 0xff) as u8,
|
|
||||||
255,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const TEXT_COLOR: UiColor = mocha(0xCDD6F4);
|
|
||||||
pub const CODE_COLOR: UiColor = mocha(0xCDD6F4);
|
|
||||||
pub const LINK_COLOR: UiColor = mocha(0x89B4FA);
|
|
||||||
pub const MARKER_COLOR: UiColor = mocha(0xB4BEFE);
|
|
||||||
pub const VERBATIM_BACKGROUND: UiColor = mocha(0x11111B);
|
|
||||||
pub const TABLE_BACKGROUND: UiColor = mocha(0x313244);
|
|
||||||
pub const QUOTE_BAR_COLOR: UiColor = mocha(0x585B70);
|
|
||||||
pub const QUOTE_TEXT_COLOR: UiColor = mocha(0xA6ADC8);
|
|
||||||
const STRIKETHROUGH_COLOR: UiColor = mocha(0x6C7086);
|
|
||||||
|
|
||||||
fn syntax_color(kind: Kind) -> UiColor {
|
|
||||||
match kind {
|
match kind {
|
||||||
Kind::Keyword => mocha(0xCBA6F7),
|
Kind::Keyword => theme.syntax_keyword.clone(),
|
||||||
Kind::String => mocha(0xA6E3A1),
|
Kind::String => theme.syntax_string.clone(),
|
||||||
Kind::Literal => mocha(0xFAB387),
|
Kind::Literal => theme.syntax_literal.clone(),
|
||||||
Kind::Comment => mocha(0x6C7086),
|
Kind::Comment => theme.syntax_comment.clone(),
|
||||||
Kind::Metadata => mocha(0xF9E2AF),
|
Kind::Metadata => theme.syntax_metadata.clone(),
|
||||||
Kind::Punctuation => mocha(0xA6ADC8),
|
Kind::Punctuation => theme.syntax_punctuation.clone(),
|
||||||
Kind::Mark => mocha(0x89DCEB),
|
Kind::Mark => theme.syntax_mark.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum BlockFrame {
|
pub enum BlockFrame {
|
||||||
Plain,
|
Plain,
|
||||||
Verbatim { fill: UiColor },
|
Verbatim { fill: PaintId },
|
||||||
Quote,
|
Quote,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn frame_of(kind: BlockKind) -> BlockFrame {
|
pub fn frame_of(kind: BlockKind, theme: &Theme) -> BlockFrame {
|
||||||
match kind {
|
match kind {
|
||||||
BlockKind::Code => BlockFrame::Verbatim {
|
BlockKind::Code => BlockFrame::Verbatim {
|
||||||
fill: VERBATIM_BACKGROUND,
|
fill: theme.verbatim_surface.clone(),
|
||||||
},
|
},
|
||||||
BlockKind::Table => BlockFrame::Verbatim {
|
BlockKind::Table => BlockFrame::Verbatim {
|
||||||
fill: TABLE_BACKGROUND,
|
fill: theme.table_surface.clone(),
|
||||||
},
|
},
|
||||||
BlockKind::Quote => BlockFrame::Quote,
|
BlockKind::Quote => BlockFrame::Quote,
|
||||||
BlockKind::Paragraph | BlockKind::Heading | BlockKind::List | BlockKind::Other => {
|
BlockKind::Paragraph | BlockKind::Heading | BlockKind::List | BlockKind::Other => {
|
||||||
@@ -114,10 +96,10 @@ fn ensure_line(out: &mut String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_block(block: &Block, base_size: f32) -> Rendered {
|
pub fn render_block(block: &Block, base_size: f32, theme: &Theme) -> Rendered {
|
||||||
match block.kind {
|
match block.kind {
|
||||||
BlockKind::Table => table_text(&block.source),
|
BlockKind::Table => table_text(&block.source, theme),
|
||||||
_ => render_markdown(&block.source, base_size),
|
_ => render_markdown(&block.source, base_size, theme),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +107,7 @@ pub fn render_block(block: &Block, base_size: f32) -> Rendered {
|
|||||||
/// style it. `base_size` is the row's ordinary paragraph font size, needed
|
/// style it. `base_size` is the row's ordinary paragraph font size, needed
|
||||||
/// only so a heading's override is relative to it rather than a hardcoded
|
/// only so a heading's override is relative to it rather than a hardcoded
|
||||||
/// absolute the caller cannot retune.
|
/// absolute the caller cannot retune.
|
||||||
pub fn render_markdown(src: &str, base_size: f32) -> Rendered {
|
pub fn render_markdown(src: &str, base_size: f32, theme: &Theme) -> Rendered {
|
||||||
let _ = base_size; // headings use the fixed Material ladder; see `heading_size`
|
let _ = base_size; // headings use the fixed Material ladder; see `heading_size`
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
let mut spans = Vec::new();
|
let mut spans = Vec::new();
|
||||||
@@ -175,7 +157,7 @@ pub fn render_markdown(src: &str, base_size: f32) -> Rendered {
|
|||||||
}
|
}
|
||||||
_ => out.push_str(BULLETS[(depth - 1) % BULLETS.len()]),
|
_ => out.push_str(BULLETS[(depth - 1) % BULLETS.len()]),
|
||||||
}
|
}
|
||||||
spans.push(SpanStyle::new(start..out.len()).color(MARKER_COLOR));
|
spans.push(SpanStyle::new(start..out.len()).color(theme.marker.clone()));
|
||||||
}
|
}
|
||||||
Tag::List(first) => lists.push(first),
|
Tag::List(first) => lists.push(first),
|
||||||
Tag::Paragraph | Tag::BlockQuote(_) => ensure_blank_line(&mut out),
|
Tag::Paragraph | Tag::BlockQuote(_) => ensure_blank_line(&mut out),
|
||||||
@@ -209,14 +191,18 @@ pub fn render_markdown(src: &str, base_size: f32) -> Rendered {
|
|||||||
TagEnd::Emphasis => spans.push(SpanStyle::new(range).italic()),
|
TagEnd::Emphasis => spans.push(SpanStyle::new(range).italic()),
|
||||||
TagEnd::Strong => spans.push(SpanStyle::new(range).bold()),
|
TagEnd::Strong => spans.push(SpanStyle::new(range).bold()),
|
||||||
TagEnd::Strikethrough => {
|
TagEnd::Strikethrough => {
|
||||||
spans.push(SpanStyle::new(range).color(STRIKETHROUGH_COLOR));
|
spans.push(SpanStyle::new(range).color(theme.strikethrough.clone()));
|
||||||
}
|
}
|
||||||
// An image draws as its alt text until the port has a
|
// An image draws as its alt text until the port has a
|
||||||
// transcript image widget (IRIS_TODO's "scaled
|
// transcript image widget (IRIS_TODO's "scaled
|
||||||
// thumbnail"); marked as a link so it is at least
|
// thumbnail"); marked as a link so it is at least
|
||||||
// followable rather than silently inert.
|
// followable rather than silently inert.
|
||||||
TagEnd::Link | TagEnd::Image => {
|
TagEnd::Link | TagEnd::Image => {
|
||||||
spans.push(SpanStyle::new(range.clone()).color(LINK_COLOR).underline());
|
spans.push(
|
||||||
|
SpanStyle::new(range.clone())
|
||||||
|
.color(theme.link.clone())
|
||||||
|
.underline(),
|
||||||
|
);
|
||||||
if let Some(url) = dest {
|
if let Some(url) = dest {
|
||||||
links.push(Link { range, url });
|
links.push(Link { range, url });
|
||||||
}
|
}
|
||||||
@@ -225,10 +211,10 @@ pub fn render_markdown(src: &str, base_size: f32) -> Rendered {
|
|||||||
spans.push(
|
spans.push(
|
||||||
SpanStyle::new(range.clone())
|
SpanStyle::new(range.clone())
|
||||||
.family(Family::Monospace)
|
.family(Family::Monospace)
|
||||||
.color(CODE_COLOR),
|
.color(theme.code.clone()),
|
||||||
);
|
);
|
||||||
if let Some(language) = fence_language.take() {
|
if let Some(language) = fence_language.take() {
|
||||||
highlight_into(&mut spans, &out, range, language);
|
highlight_into(&mut spans, &out, range, language, theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => unreachable!("filtered by the outer match arm"),
|
_ => unreachable!("filtered by the outer match arm"),
|
||||||
@@ -241,7 +227,7 @@ pub fn render_markdown(src: &str, base_size: f32) -> Rendered {
|
|||||||
spans.push(
|
spans.push(
|
||||||
SpanStyle::new(start..out.len())
|
SpanStyle::new(start..out.len())
|
||||||
.family(Family::Monospace)
|
.family(Family::Monospace)
|
||||||
.color(CODE_COLOR),
|
.color(theme.code.clone()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Event::SoftBreak => out.push(' '),
|
Event::SoftBreak => out.push(' '),
|
||||||
@@ -253,7 +239,7 @@ pub fn render_markdown(src: &str, base_size: f32) -> Rendered {
|
|||||||
Event::TaskListMarker(done) => {
|
Event::TaskListMarker(done) => {
|
||||||
let start = out.len();
|
let start = out.len();
|
||||||
out.push_str(if done { "[x] " } else { "[ ] " });
|
out.push_str(if done { "[x] " } else { "[ ] " });
|
||||||
spans.push(SpanStyle::new(start..out.len()).color(MARKER_COLOR));
|
spans.push(SpanStyle::new(start..out.len()).color(theme.marker.clone()));
|
||||||
}
|
}
|
||||||
Event::End(TagEnd::List(_)) => {
|
Event::End(TagEnd::List(_)) => {
|
||||||
lists.pop();
|
lists.pop();
|
||||||
@@ -282,6 +268,7 @@ pub(crate) fn highlight_into(
|
|||||||
text: &str,
|
text: &str,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
language: Language,
|
language: Language,
|
||||||
|
theme: &Theme,
|
||||||
) {
|
) {
|
||||||
let code = &text[range.clone()];
|
let code = &text[range.clone()];
|
||||||
// char index -> byte offset within `code`, plus the end, so a span's
|
// char index -> byte offset within `code`, plus the end, so a span's
|
||||||
@@ -305,14 +292,14 @@ pub(crate) fn highlight_into(
|
|||||||
spans.push(
|
spans.push(
|
||||||
SpanStyle::new(range.start + start..range.start + end)
|
SpanStyle::new(range.start + start..range.start + end)
|
||||||
.family(Family::Monospace)
|
.family(Family::Monospace)
|
||||||
.color(syntax_color(span.kind)),
|
.color(syntax_color(span.kind, theme)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TABLE_MAX_COL: usize = 28;
|
const TABLE_MAX_COL: usize = 28;
|
||||||
|
|
||||||
pub fn table_text(src: &str) -> Rendered {
|
pub fn table_text(src: &str, theme: &Theme) -> Rendered {
|
||||||
let rows = table_cells(src);
|
let rows = table_cells(src);
|
||||||
if rows.is_empty() {
|
if rows.is_empty() {
|
||||||
return Rendered::default();
|
return Rendered::default();
|
||||||
@@ -363,7 +350,7 @@ pub fn table_text(src: &str) -> Rendered {
|
|||||||
let rule: usize = widths.iter().sum::<usize>() + 2 * widths.len().saturating_sub(1);
|
let rule: usize = widths.iter().sum::<usize>() + 2 * widths.len().saturating_sub(1);
|
||||||
let rule_start = out.len();
|
let rule_start = out.len();
|
||||||
out.extend(std::iter::repeat_n('\u{2500}', rule));
|
out.extend(std::iter::repeat_n('\u{2500}', rule));
|
||||||
spans.push(SpanStyle::new(rule_start..out.len()).color(QUOTE_BAR_COLOR));
|
spans.push(SpanStyle::new(rule_start..out.len()).color(theme.quote_bar.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Rendered {
|
Rendered {
|
||||||
@@ -421,6 +408,36 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::client::markdown_blocks::split_blocks;
|
use crate::client::markdown_blocks::split_blocks;
|
||||||
|
|
||||||
|
fn with_theme<T>(f: impl FnOnce(&Theme) -> T) -> T {
|
||||||
|
let mut paints = Paints::new();
|
||||||
|
let theme = Theme::new(&mut paints);
|
||||||
|
f(&theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_markdown(src: &str, base_size: f32) -> Rendered {
|
||||||
|
with_theme(|theme| super::render_markdown(src, base_size, theme))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_block(block: &Block, base_size: f32) -> Rendered {
|
||||||
|
with_theme(|theme| super::render_block(block, base_size, theme))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame_of(kind: BlockKind) -> BlockFrame {
|
||||||
|
with_theme(|theme| super::frame_of(kind, theme))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syntax_color(kind: Kind) -> PaintId {
|
||||||
|
with_theme(|theme| super::syntax_color(kind, theme))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn code_color() -> PaintId {
|
||||||
|
with_theme(|theme| theme.code.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn marker_color() -> PaintId {
|
||||||
|
with_theme(|theme| theme.marker.clone())
|
||||||
|
}
|
||||||
|
|
||||||
fn block(src: &str) -> Rendered {
|
fn block(src: &str) -> Rendered {
|
||||||
let blocks = split_blocks(src);
|
let blocks = split_blocks(src);
|
||||||
assert_eq!(blocks.len(), 1, "test wants exactly one block: {blocks:?}");
|
assert_eq!(blocks.len(), 1, "test wants exactly one block: {blocks:?}");
|
||||||
@@ -509,7 +526,7 @@ mod tests {
|
|||||||
assert_eq!(r.text, "let x = 1;");
|
assert_eq!(r.text, "let x = 1;");
|
||||||
assert_eq!(r.spans.len(), 1);
|
assert_eq!(r.spans.len(), 1);
|
||||||
assert!(r.spans[0].family == Some(Family::Monospace));
|
assert!(r.spans[0].family == Some(Family::Monospace));
|
||||||
assert_eq!(r.spans[0].color, Some(CODE_COLOR));
|
assert_eq!(r.spans[0].color, Some(code_color()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -550,7 +567,7 @@ mod tests {
|
|||||||
let markers: Vec<_> = r
|
let markers: Vec<_> = r
|
||||||
.spans
|
.spans
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|s| s.color == Some(MARKER_COLOR))
|
.filter(|s| s.color == Some(marker_color()))
|
||||||
.map(|s| r.text[s.range.clone()].to_string())
|
.map(|s| r.text[s.range.clone()].to_string())
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(markers, ["\u{2022} ", "\u{2022} ", "\u{25e6} "]);
|
assert_eq!(markers, ["\u{2022} ", "\u{2022} ", "\u{25e6} "]);
|
||||||
@@ -563,7 +580,7 @@ mod tests {
|
|||||||
let markers: Vec<_> = r
|
let markers: Vec<_> = r
|
||||||
.spans
|
.spans
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|s| s.color == Some(MARKER_COLOR))
|
.filter(|s| s.color == Some(marker_color()))
|
||||||
.map(|s| r.text[s.range.clone()].to_string())
|
.map(|s| r.text[s.range.clone()].to_string())
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(markers, ["3. ", "4. "]);
|
assert_eq!(markers, ["3. ", "4. "]);
|
||||||
|
|||||||
+41
-139
@@ -7,14 +7,14 @@ pub mod composer;
|
|||||||
pub mod fixture;
|
pub mod fixture;
|
||||||
pub mod markdown;
|
pub mod markdown;
|
||||||
pub mod row;
|
pub mod row;
|
||||||
pub mod selection;
|
|
||||||
pub(crate) mod tap;
|
pub(crate) mod tap;
|
||||||
|
pub mod theme;
|
||||||
pub mod tool;
|
pub mod tool;
|
||||||
|
|
||||||
use crate::client::transcript_fold::TranscriptRow as FoldedRow;
|
use crate::client::transcript_fold::TranscriptRow as FoldedRow;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use selection::Selection;
|
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
use theme::Theme;
|
||||||
|
|
||||||
pub struct TranscriptScreen {
|
pub struct TranscriptScreen {
|
||||||
/// The transcript's own `LazySpan` -- the layout *and* the scroll
|
/// The transcript's own `LazySpan` -- the layout *and* the scroll
|
||||||
@@ -25,10 +25,10 @@ pub struct TranscriptScreen {
|
|||||||
/// `.jump_to_end()` directly.
|
/// `.jump_to_end()` directly.
|
||||||
pub list: WeakWidget<LazySpan>,
|
pub list: WeakWidget<LazySpan>,
|
||||||
pub composer: composer::Composer,
|
pub composer: composer::Composer,
|
||||||
selection: Rc<RefCell<Selection>>,
|
|
||||||
rebuilds: std::cell::Cell<usize>,
|
rebuilds: std::cell::Cell<usize>,
|
||||||
tail: RefCell<Option<(RowKey, row::TailRow)>>,
|
tail: RefCell<Option<(RowKey, row::TailRow)>>,
|
||||||
session_working: std::cell::Cell<bool>,
|
session_working: std::cell::Cell<bool>,
|
||||||
|
theme: Rc<Theme>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TranscriptScreen {
|
impl TranscriptScreen {
|
||||||
@@ -48,10 +48,10 @@ impl TranscriptScreen {
|
|||||||
let (key, widget, tail) = row::build_row(
|
let (key, widget, tail) = row::build_row(
|
||||||
rsc,
|
rsc,
|
||||||
self.list,
|
self.list,
|
||||||
self.selection.clone(),
|
|
||||||
row,
|
row,
|
||||||
self.session_working.get(),
|
self.session_working.get(),
|
||||||
true,
|
true,
|
||||||
|
self.theme.clone(),
|
||||||
);
|
);
|
||||||
(self.list)(rsc).push_back(LazyItem::new(key, widget));
|
(self.list)(rsc).push_back(LazyItem::new(key, widget));
|
||||||
*self.tail.borrow_mut() = tail.map(|t| (key, t));
|
*self.tail.borrow_mut() = tail.map(|t| (key, t));
|
||||||
@@ -122,14 +122,7 @@ impl TranscriptScreen {
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
blocks.apply_delta(
|
blocks.apply_delta(rsc, sender, &markdown_src)
|
||||||
rsc,
|
|
||||||
self.list,
|
|
||||||
self.selection.clone(),
|
|
||||||
key,
|
|
||||||
sender,
|
|
||||||
&markdown_src,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
(row::TailRow::Tools(tools), FoldedRow::Tools(calls)) => {
|
(row::TailRow::Tools(tools), FoldedRow::Tools(calls)) => {
|
||||||
tools.apply_calls(rsc, calls, self.session_working.get())
|
tools.apply_calls(rsc, calls, self.session_working.get())
|
||||||
@@ -179,14 +172,13 @@ impl TranscriptScreen {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.selection.borrow_mut().unregister(old_key);
|
|
||||||
let (new_key, widget, kept) = row::build_row(
|
let (new_key, widget, kept) = row::build_row(
|
||||||
rsc,
|
rsc,
|
||||||
self.list,
|
self.list,
|
||||||
self.selection.clone(),
|
|
||||||
&new_rows[common],
|
&new_rows[common],
|
||||||
self.session_working.get(),
|
self.session_working.get(),
|
||||||
false,
|
false,
|
||||||
|
self.theme.clone(),
|
||||||
);
|
);
|
||||||
let evicted = (self.list)(rsc).replace_back(LazyItem::new(new_key, widget));
|
let evicted = (self.list)(rsc).replace_back(LazyItem::new(new_key, widget));
|
||||||
drop(evicted); // frees the old row's widget, same as a pop would
|
drop(evicted); // frees the old row's widget, same as a pop would
|
||||||
@@ -197,7 +189,6 @@ impl TranscriptScreen {
|
|||||||
}
|
}
|
||||||
RowDiff::Rebuild => {
|
RowDiff::Rebuild => {
|
||||||
self.rebuilds.set(self.rebuilds.get() + 1);
|
self.rebuilds.set(self.rebuilds.get() + 1);
|
||||||
self.selection.borrow_mut().clear();
|
|
||||||
(self.list)(rsc).clear();
|
(self.list)(rsc).clear();
|
||||||
*self.tail.borrow_mut() = None;
|
*self.tail.borrow_mut() = None;
|
||||||
for row in &new_rows {
|
for row in &new_rows {
|
||||||
@@ -211,10 +202,23 @@ impl TranscriptScreen {
|
|||||||
self.rebuilds.replace(0)
|
self.rebuilds.replace(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The semantic paint IDs used by this screen. A caller can replace
|
||||||
|
/// their entries through `rsc.ui_mut().paints.set(...)`; retained text
|
||||||
|
/// and rect primitives keep the IDs and need no widget rebuild.
|
||||||
|
pub fn theme(&self) -> &Theme {
|
||||||
|
&self.theme
|
||||||
|
}
|
||||||
|
|
||||||
/// The concatenated text of whatever is currently selected across one
|
/// The concatenated text of whatever is currently selected across one
|
||||||
/// or more rows, `None` if nothing is -- what a copy command reads.
|
/// or more rows, `None` if nothing is -- what a copy command reads.
|
||||||
pub fn selected_text(&self, rsc: &mut impl UiRsc) -> Option<String> {
|
pub fn selected_text<Rsc: HasEvents>(&self, rsc: &mut Rsc) -> Option<String> {
|
||||||
self.selection.borrow().selected_text(rsc)
|
let id = rsc
|
||||||
|
.events()
|
||||||
|
.controllers
|
||||||
|
.id::<SelectionController>(self.list.id())?;
|
||||||
|
rsc.with_controller::<SelectionController, _>(id, |selection, rsc| {
|
||||||
|
selection.selected_text(rsc)
|
||||||
|
})?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,8 +242,14 @@ pub fn build_tree<Rsc: HasEvents>(
|
|||||||
where
|
where
|
||||||
Rsc::State: FocusHost + OpenUrl,
|
Rsc::State: FocusHost + OpenUrl,
|
||||||
{
|
{
|
||||||
let selection = Rc::new(RefCell::new(Selection::new()));
|
let theme = Rc::new(Theme::new(&mut rsc.ui_mut().paints));
|
||||||
let list = LazySpan::new(Dir::DOWN, Pin::End).add(rsc);
|
let list = LazySpan::new(Dir::DOWN, Pin::End).add(rsc);
|
||||||
|
list.controller(
|
||||||
|
SelectionController::new()
|
||||||
|
.with_scroll(list)
|
||||||
|
.separator("\n\n"),
|
||||||
|
)
|
||||||
|
.add(rsc);
|
||||||
|
|
||||||
// The last row's block widgets are kept for the same reason
|
// The last row's block widgets are kept for the same reason
|
||||||
// `push_row` keeps them: a reply that is *already* streaming when the
|
// `push_row` keeps them: a reply that is *already* streaming when the
|
||||||
@@ -258,39 +268,24 @@ where
|
|||||||
// be a reply already streaming when this screen opened, and a
|
// be a reply already streaming when this screen opened, and a
|
||||||
// capped row cannot take a delta (`RowBlocks::capped`).
|
// capped row cannot take a delta (`RowBlocks::capped`).
|
||||||
let cap = i + 1 < rows.len();
|
let cap = i + 1 < rows.len();
|
||||||
let (key, widget, kept) = row::build_row(rsc, list, selection.clone(), row, false, cap);
|
let (key, widget, kept) = row::build_row(rsc, list, row, false, cap, theme.clone());
|
||||||
list(rsc).push_back(LazyItem::new(key, widget));
|
list(rsc).push_back(LazyItem::new(key, widget));
|
||||||
tail = kept.map(|t| (key, t));
|
tail = kept.map(|t| (key, t));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The continuation of a row-started drag once it has committed and
|
// The controller host covers gaps as well as text, so a tap anywhere in
|
||||||
// taken pointer capture on `list`'s own id (`row.rs`'s registration is
|
// the transcript can dismiss a selection. Text and link listeners may
|
||||||
// only ever the gesture's first frame) -- registered once here, not
|
// see the same physical sample; `SelectionController` deduplicates it by
|
||||||
// once per row, since `DragGesture`'s single shared instance must see
|
// the sample's own timestamp while still returning the same tap decision
|
||||||
// each frame of one gesture exactly once. `ctx.data.pos`/`size` are
|
// to whichever leaf owns the link action.
|
||||||
// already relative to `list`'s own on-screen box (this is what it was
|
|
||||||
// registered against), which is exactly the viewport-pixel space
|
|
||||||
// `LazySpan::key_at`/`extent` work in, so the row-under-the-pointer is
|
|
||||||
// resolved from those instead of a per-row hit test.
|
|
||||||
{
|
{
|
||||||
let selection = selection.clone();
|
list.on(CursorSense::drag_senses(), move |ctx, rsc: &mut Rsc| {
|
||||||
list.on(
|
let input = &ctx.data;
|
||||||
CursorSense::Pressing(CursorButton::Left) | CursorSense::Drop | CursorSense::Cancel,
|
rsc.with_nearest_controller::<SelectionController, _>(
|
||||||
move |ctx, rsc| {
|
|
||||||
let row = selection
|
|
||||||
.borrow()
|
|
||||||
.locate(&*rsc, ctx.data.render, ctx.data.cursor.pos);
|
|
||||||
selection.borrow_mut().drag(
|
|
||||||
rsc,
|
|
||||||
list,
|
list,
|
||||||
row,
|
|id, selection, rsc| selection.drag(id, rsc, input),
|
||||||
ctx.data.cursor.pos,
|
|
||||||
ctx.data.sense,
|
|
||||||
ctx.data.cursor.time,
|
|
||||||
ctx.data.pointer,
|
|
||||||
);
|
);
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,9 +294,8 @@ where
|
|||||||
ctx.widget(rsc).scroll(delta);
|
ctx.widget(rsc).scroll(delta);
|
||||||
})
|
})
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
selection.borrow_mut().set_scroll_area(list);
|
|
||||||
|
|
||||||
let (composer, composer_bar) = composer::build_composer(rsc);
|
let (composer, composer_bar) = composer::build_composer(rsc, &theme);
|
||||||
|
|
||||||
let tree = (list.width(rest(1)).height(rest(1)).masked(), composer_bar)
|
let tree = (list.width(rest(1)).height(rest(1)).masked(), composer_bar)
|
||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
@@ -314,8 +308,8 @@ where
|
|||||||
session_working: std::cell::Cell::new(false),
|
session_working: std::cell::Cell::new(false),
|
||||||
list,
|
list,
|
||||||
composer,
|
composer,
|
||||||
selection,
|
|
||||||
rebuilds: std::cell::Cell::new(0),
|
rebuilds: std::cell::Cell::new(0),
|
||||||
|
theme,
|
||||||
},
|
},
|
||||||
tree,
|
tree,
|
||||||
)
|
)
|
||||||
@@ -503,21 +497,6 @@ mod apply_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tool(seq: u64, run_id: &str) -> TranscriptItem {
|
|
||||||
TranscriptItem::ToolRun {
|
|
||||||
seq,
|
|
||||||
id: format!("id{seq}"),
|
|
||||||
run_id: run_id.to_string(),
|
|
||||||
tool: "grep".to_string(),
|
|
||||||
input: "x".to_string(),
|
|
||||||
output: String::new(),
|
|
||||||
done: false,
|
|
||||||
failed: false,
|
|
||||||
asks: Vec::new(),
|
|
||||||
images: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assistant(seq: u64, text: &str) -> TranscriptItem {
|
fn assistant(seq: u64, text: &str) -> TranscriptItem {
|
||||||
TranscriptItem::AssistantMsg {
|
TranscriptItem::AssistantMsg {
|
||||||
seq,
|
seq,
|
||||||
@@ -582,83 +561,6 @@ mod apply_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_row_dropped_by_a_regroup_does_not_outlive_itself_in_selection() {
|
|
||||||
use crate::client::transcript_fold::group_tool_runs;
|
|
||||||
|
|
||||||
let mut rsc = TestRsc {
|
|
||||||
ui: UiData::default(),
|
|
||||||
events: EventManager::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let old_items = vec![tool(1, "run-a"), user(2, "meanwhile"), user(4, "stable")];
|
|
||||||
let new_items = vec![tool(1, "run-a"), tool(3, "run-a"), user(4, "stable")];
|
|
||||||
assert_eq!(
|
|
||||||
diff_rows(&group_tool_runs(&old_items), &group_tool_runs(&new_items)),
|
|
||||||
RowDiff::Rebuild,
|
|
||||||
"test setup must actually exercise the Rebuild arm"
|
|
||||||
);
|
|
||||||
|
|
||||||
let (screen, _tree) = build_tree(&mut rsc, group_tool_runs(&old_items));
|
|
||||||
screen.apply(&mut rsc, &old_items, &new_items);
|
|
||||||
|
|
||||||
let surviving_key = row::row_key(&crate::client::transcript_fold::ItemKey::Seq(4));
|
|
||||||
screen.selection.borrow_mut().begin(
|
|
||||||
&mut rsc,
|
|
||||||
(surviving_key, 0),
|
|
||||||
Vec2::ZERO,
|
|
||||||
Vec2::new(10.0, 10.0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_tail_rebuilt_with_fewer_blocks_leaves_none_of_them_in_selection() {
|
|
||||||
use crate::client::transcript_fold::group_tool_runs;
|
|
||||||
|
|
||||||
let mut rsc = TestRsc {
|
|
||||||
ui: UiData::default(),
|
|
||||||
events: EventManager::default(),
|
|
||||||
};
|
|
||||||
let old_items = vec![user(1, "stable"), assistant(2, "# Head\n\npara\n\n- item")];
|
|
||||||
let new_items = vec![user(1, "stable"), assistant(2, "short")];
|
|
||||||
assert_eq!(
|
|
||||||
diff_rows(&group_tool_runs(&old_items), &group_tool_runs(&new_items)),
|
|
||||||
RowDiff::ReplaceLast { common: 1 },
|
|
||||||
"test setup must actually exercise the ReplaceLast arm"
|
|
||||||
);
|
|
||||||
|
|
||||||
let (screen, _tree) = build_tree(&mut rsc, group_tool_runs(&old_items));
|
|
||||||
let tail_key = row::row_key(&crate::client::transcript_fold::ItemKey::Seq(2));
|
|
||||||
assert_eq!(
|
|
||||||
screen
|
|
||||||
.selection
|
|
||||||
.borrow()
|
|
||||||
.registered_blocks(tail_key)
|
|
||||||
.count(),
|
|
||||||
3,
|
|
||||||
"the fixture must start with more blocks than it ends with"
|
|
||||||
);
|
|
||||||
|
|
||||||
screen.apply(&mut rsc, &old_items, &new_items);
|
|
||||||
assert_eq!(
|
|
||||||
screen
|
|
||||||
.selection
|
|
||||||
.borrow()
|
|
||||||
.registered_blocks(tail_key)
|
|
||||||
.count(),
|
|
||||||
1,
|
|
||||||
"the blocks the rebuild dropped are still registered"
|
|
||||||
);
|
|
||||||
|
|
||||||
let surviving_key = row::row_key(&crate::client::transcript_fold::ItemKey::Seq(1));
|
|
||||||
screen.selection.borrow_mut().begin(
|
|
||||||
&mut rsc,
|
|
||||||
(surviving_key, 0),
|
|
||||||
Vec2::ZERO,
|
|
||||||
Vec2::new(10.0, 10.0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn call(id: &str, output: &str, done: bool) -> TranscriptItem {
|
fn call(id: &str, output: &str, done: bool) -> TranscriptItem {
|
||||||
TranscriptItem::ToolRun {
|
TranscriptItem::ToolRun {
|
||||||
seq: 1,
|
seq: 1,
|
||||||
|
|||||||
+44
-60
@@ -2,8 +2,8 @@ use crate::client::markdown_blocks::{Block, BlockKind, common_prefix, split_bloc
|
|||||||
use crate::client::text_cap::{MESSAGE_BYTES, MESSAGE_LINES, cut, show_all_label};
|
use crate::client::text_cap::{MESSAGE_BYTES, MESSAGE_LINES, cut, show_all_label};
|
||||||
use crate::client::transcript_fold::{QuestionCard, TranscriptItem, TranscriptRow as FoldedRow};
|
use crate::client::transcript_fold::{QuestionCard, TranscriptItem, TranscriptRow as FoldedRow};
|
||||||
use crate::ui::markdown::{BlockFrame, Link, frame_of, render_block};
|
use crate::ui::markdown::{BlockFrame, Link, frame_of, render_block};
|
||||||
use crate::ui::selection::{SelKey, Selection};
|
|
||||||
use crate::ui::tap::{hold_edge, on_tap};
|
use crate::ui::tap::{hold_edge, on_tap};
|
||||||
|
use crate::ui::theme::Theme;
|
||||||
use crate::ui::tool::ToolRow;
|
use crate::ui::tool::ToolRow;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
@@ -93,7 +93,7 @@ fn tool_call_markdown(tool: &str, input: &str, output: &str) -> String {
|
|||||||
|
|
||||||
pub struct RowBlocks {
|
pub struct RowBlocks {
|
||||||
blocks: Vec<Block>,
|
blocks: Vec<Block>,
|
||||||
fields: Vec<WeakWidget<TextEdit>>,
|
fields: Vec<WeakWidget<Text>>,
|
||||||
links: Vec<Rc<RefCell<Vec<Link>>>>,
|
links: Vec<Rc<RefCell<Vec<Link>>>>,
|
||||||
column: WeakWidget<Span>,
|
column: WeakWidget<Span>,
|
||||||
sender: Option<String>,
|
sender: Option<String>,
|
||||||
@@ -108,6 +108,7 @@ pub struct RowBlocks {
|
|||||||
/// keeps the refusal from costing anything in practice. This field is
|
/// keeps the refusal from costing anything in practice. This field is
|
||||||
/// the belt to that braces.
|
/// the belt to that braces.
|
||||||
capped: bool,
|
capped: bool,
|
||||||
|
theme: Rc<Theme>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Split for display: never empty, so a row with nothing in it yet is
|
/// Split for display: never empty, so a row with nothing in it yet is
|
||||||
@@ -181,21 +182,18 @@ const FRAME_RADIUS_DP: f32 = 8.0;
|
|||||||
|
|
||||||
fn build_block<Rsc: HasEvents>(
|
fn build_block<Rsc: HasEvents>(
|
||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
list: WeakWidget<LazySpan>,
|
|
||||||
selection: Rc<RefCell<Selection>>,
|
|
||||||
key: SelKey,
|
|
||||||
block: &Block,
|
block: &Block,
|
||||||
) -> (WeakWidget<TextEdit>, StrongWidget, Rc<RefCell<Vec<Link>>>)
|
theme: &Theme,
|
||||||
|
) -> (WeakWidget<Text>, StrongWidget, Rc<RefCell<Vec<Link>>>)
|
||||||
where
|
where
|
||||||
Rsc::State: FocusHost + OpenUrl,
|
Rsc::State: FocusHost + OpenUrl,
|
||||||
{
|
{
|
||||||
let frame = frame_of(block.kind);
|
let frame = frame_of(block.kind, theme);
|
||||||
let rendered = render_block(block, BASE_SIZE);
|
let rendered = render_block(block, BASE_SIZE, theme);
|
||||||
let links = Rc::new(RefCell::new(rendered.links));
|
let links = Rc::new(RefCell::new(rendered.links));
|
||||||
let verbatim = matches!(frame, BlockFrame::Verbatim { .. });
|
let verbatim = matches!(frame, BlockFrame::Verbatim { .. });
|
||||||
let field = wtext(rendered.text)
|
let field = wtext(rendered.text)
|
||||||
.spans(rendered.spans)
|
.spans(rendered.spans)
|
||||||
.editable(EditMode::MultiLine)
|
|
||||||
.text_align(Align::LEFT)
|
.text_align(Align::LEFT)
|
||||||
.wrap(!verbatim)
|
.wrap(!verbatim)
|
||||||
.family(if verbatim {
|
.family(if verbatim {
|
||||||
@@ -205,31 +203,28 @@ where
|
|||||||
})
|
})
|
||||||
.size(BASE_SIZE)
|
.size(BASE_SIZE)
|
||||||
.color(match frame {
|
.color(match frame {
|
||||||
BlockFrame::Quote => crate::ui::markdown::QUOTE_TEXT_COLOR,
|
BlockFrame::Quote => theme.quote_text.clone(),
|
||||||
_ => crate::ui::markdown::TEXT_COLOR,
|
_ => theme.text.clone(),
|
||||||
})
|
})
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
selection.borrow_mut().register(key, field);
|
|
||||||
|
|
||||||
let tap_links = links.clone();
|
let tap_links = links.clone();
|
||||||
field
|
field
|
||||||
.on(CursorSense::drag_senses(), move |ctx, rsc| {
|
.on(CursorSense::drag_senses(), move |ctx, rsc: &mut Rsc| {
|
||||||
let (pos, size, cursor) = (ctx.data.pos, ctx.data.size, ctx.data.cursor.pos);
|
let (pos, size) = (ctx.data.pos, ctx.data.size);
|
||||||
let outcome = selection.borrow_mut().drag(
|
let input = &ctx.data;
|
||||||
rsc,
|
let outcome = rsc
|
||||||
list,
|
.with_nearest_controller::<SelectionController, _>(
|
||||||
Some((key, pos, size)),
|
field,
|
||||||
cursor,
|
|id, selection, rsc| selection.drag(id, rsc, input),
|
||||||
ctx.data.sense,
|
)
|
||||||
ctx.data.cursor.time,
|
.unwrap_or(SelectionInput::Tapped);
|
||||||
ctx.data.pointer,
|
|
||||||
);
|
|
||||||
// A *tap*, decided by the same `DragArbiter` the pan and
|
// A *tap*, decided by the same `DragArbiter` the pan and
|
||||||
// the selection are: a gesture that panned the list past
|
// the selection are: a gesture that panned the list past
|
||||||
// this link, or held long enough to select, must not also
|
// this link, or held long enough to select, must not also
|
||||||
// follow it (`GestureOutcome::Tapped`'s doc).
|
// follow it (`GestureOutcome::Tapped`'s doc).
|
||||||
if outcome == GestureOutcome::Tapped {
|
if outcome == SelectionInput::Tapped {
|
||||||
let byte = field.edit(rsc).byte_at(cursor, size);
|
let byte = field.selection(rsc).byte_at(pos, size);
|
||||||
let url = tap_links
|
let url = tap_links
|
||||||
.borrow()
|
.borrow()
|
||||||
.iter()
|
.iter()
|
||||||
@@ -258,7 +253,7 @@ where
|
|||||||
left: dp(QUOTE_BAR_DP + FRAME_PAD_DP),
|
left: dp(QUOTE_BAR_DP + FRAME_PAD_DP),
|
||||||
..Padding::ZERO
|
..Padding::ZERO
|
||||||
})
|
})
|
||||||
.background(rect(crate::ui::markdown::QUOTE_BAR_COLOR).width(dp(QUOTE_BAR_DP)))
|
.background(rect(theme.quote_bar.clone()).width(dp(QUOTE_BAR_DP)))
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any(),
|
.any(),
|
||||||
@@ -266,14 +261,15 @@ where
|
|||||||
(field, framed, links)
|
(field, framed, links)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn build_text_row<Rsc: HasEvents>(
|
fn build_text_row<Rsc: HasEvents>(
|
||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
list: WeakWidget<LazySpan>,
|
list: WeakWidget<LazySpan>,
|
||||||
selection: Rc<RefCell<Selection>>,
|
|
||||||
key: RowKey,
|
key: RowKey,
|
||||||
sender: Option<&str>,
|
sender: Option<&str>,
|
||||||
markdown_src: &str,
|
markdown_src: &str,
|
||||||
cap: bool,
|
cap: bool,
|
||||||
|
theme: Rc<Theme>,
|
||||||
) -> (StrongWidget, RowBlocks)
|
) -> (StrongWidget, RowBlocks)
|
||||||
where
|
where
|
||||||
Rsc::State: FocusHost + OpenUrl,
|
Rsc::State: FocusHost + OpenUrl,
|
||||||
@@ -284,7 +280,7 @@ where
|
|||||||
});
|
});
|
||||||
let strong = WidgetPtr::new().add_strong(rsc);
|
let strong = WidgetPtr::new().add_strong(rsc);
|
||||||
let ptr = strong.weak();
|
let ptr = strong.weak();
|
||||||
let (content, blocks) = row_content(rsc, list, selection, key, source, ptr, cap);
|
let (content, blocks) = row_content(rsc, list, key, source, ptr, cap, theme);
|
||||||
ptr(rsc).set(content);
|
ptr(rsc).set(content);
|
||||||
(strong.any(), blocks)
|
(strong.any(), blocks)
|
||||||
}
|
}
|
||||||
@@ -292,14 +288,15 @@ where
|
|||||||
/// Separate from [`build_text_row`] because the tap calls it a second
|
/// Separate from [`build_text_row`] because the tap calls it a second
|
||||||
/// time, with `cap` false, and writes the result back into the same
|
/// time, with `cap` false, and writes the result back into the same
|
||||||
/// `WidgetPtr`.
|
/// `WidgetPtr`.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn row_content<Rsc: HasEvents>(
|
fn row_content<Rsc: HasEvents>(
|
||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
list: WeakWidget<LazySpan>,
|
list: WeakWidget<LazySpan>,
|
||||||
selection: Rc<RefCell<Selection>>,
|
|
||||||
key: RowKey,
|
key: RowKey,
|
||||||
source: Rc<RowSource>,
|
source: Rc<RowSource>,
|
||||||
ptr: WeakWidget<WidgetPtr>,
|
ptr: WeakWidget<WidgetPtr>,
|
||||||
cap: bool,
|
cap: bool,
|
||||||
|
theme: Rc<Theme>,
|
||||||
) -> (StrongWidget, RowBlocks)
|
) -> (StrongWidget, RowBlocks)
|
||||||
where
|
where
|
||||||
Rsc::State: FocusHost + OpenUrl,
|
Rsc::State: FocusHost + OpenUrl,
|
||||||
@@ -308,9 +305,8 @@ where
|
|||||||
let mut column = Span::empty(Dir::DOWN).gap(dp(BLOCK_GAP_DP));
|
let mut column = Span::empty(Dir::DOWN).gap(dp(BLOCK_GAP_DP));
|
||||||
let mut fields = Vec::with_capacity(blocks.len());
|
let mut fields = Vec::with_capacity(blocks.len());
|
||||||
let mut links = Vec::with_capacity(blocks.len());
|
let mut links = Vec::with_capacity(blocks.len());
|
||||||
for (i, block) in blocks.iter().enumerate() {
|
for block in &blocks {
|
||||||
let (field, framed, block_links) =
|
let (field, framed, block_links) = build_block(rsc, block, &theme);
|
||||||
build_block(rsc, list, selection.clone(), (key, i as u32), block);
|
|
||||||
fields.push(field);
|
fields.push(field);
|
||||||
links.push(block_links);
|
links.push(block_links);
|
||||||
column.push(framed);
|
column.push(framed);
|
||||||
@@ -319,11 +315,11 @@ where
|
|||||||
column.push(show_all(
|
column.push(show_all(
|
||||||
rsc,
|
rsc,
|
||||||
list,
|
list,
|
||||||
selection.clone(),
|
|
||||||
key,
|
key,
|
||||||
source.clone(),
|
source.clone(),
|
||||||
ptr,
|
ptr,
|
||||||
lines,
|
lines,
|
||||||
|
theme.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let column = column.add(rsc);
|
let column = column.add(rsc);
|
||||||
@@ -339,7 +335,7 @@ where
|
|||||||
let header: WeakWidget = match &source.sender {
|
let header: WeakWidget = match &source.sender {
|
||||||
Some(name) => wtext(name.clone())
|
Some(name) => wtext(name.clone())
|
||||||
.size(13.0)
|
.size(13.0)
|
||||||
.color(UiColor::new(150, 150, 160, 255))
|
.color(theme.secondary_text.clone())
|
||||||
.add(rsc),
|
.add(rsc),
|
||||||
None => Span::empty(Dir::DOWN).add(rsc),
|
None => Span::empty(Dir::DOWN).add(rsc),
|
||||||
};
|
};
|
||||||
@@ -359,6 +355,7 @@ where
|
|||||||
column,
|
column,
|
||||||
sender: source.sender.clone(),
|
sender: source.sender.clone(),
|
||||||
capped: hidden.is_some(),
|
capped: hidden.is_some(),
|
||||||
|
theme,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -370,11 +367,11 @@ where
|
|||||||
fn show_all<Rsc: HasEvents>(
|
fn show_all<Rsc: HasEvents>(
|
||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
list: WeakWidget<LazySpan>,
|
list: WeakWidget<LazySpan>,
|
||||||
selection: Rc<RefCell<Selection>>,
|
|
||||||
key: RowKey,
|
key: RowKey,
|
||||||
source: Rc<RowSource>,
|
source: Rc<RowSource>,
|
||||||
ptr: WeakWidget<WidgetPtr>,
|
ptr: WeakWidget<WidgetPtr>,
|
||||||
lines: usize,
|
lines: usize,
|
||||||
|
theme: Rc<Theme>,
|
||||||
) -> StrongWidget
|
) -> StrongWidget
|
||||||
where
|
where
|
||||||
Rsc::State: FocusHost + OpenUrl,
|
Rsc::State: FocusHost + OpenUrl,
|
||||||
@@ -384,22 +381,15 @@ where
|
|||||||
let more = more_strong.weak();
|
let more = more_strong.weak();
|
||||||
let words = wtext(label.clone())
|
let words = wtext(label.clone())
|
||||||
.size(13.0)
|
.size(13.0)
|
||||||
.color(UiColor::new(150, 150, 160, 255))
|
.color(theme.secondary_text.clone())
|
||||||
.text_align(Align::LEFT)
|
.text_align(Align::LEFT)
|
||||||
.label(label)
|
.label(label)
|
||||||
.add_strong(rsc);
|
.add_strong(rsc);
|
||||||
more(rsc).set(words);
|
more(rsc).set(words);
|
||||||
on_tap(rsc, more, list, selection.clone(), move |rsc| {
|
on_tap(rsc, more, list, move |rsc| {
|
||||||
hold_edge(rsc, list, key);
|
hold_edge(rsc, list, key);
|
||||||
let (content, _blocks) = row_content(
|
let (content, _blocks) =
|
||||||
rsc,
|
row_content(rsc, list, key, source.clone(), ptr, false, theme.clone());
|
||||||
list,
|
|
||||||
selection.clone(),
|
|
||||||
key,
|
|
||||||
source.clone(),
|
|
||||||
ptr,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
let _old = ptr(rsc).replace(content);
|
let _old = ptr(rsc).replace(content);
|
||||||
});
|
});
|
||||||
more_strong.any()
|
more_strong.any()
|
||||||
@@ -415,9 +405,6 @@ impl RowBlocks {
|
|||||||
pub fn apply_delta<Rsc: HasEvents>(
|
pub fn apply_delta<Rsc: HasEvents>(
|
||||||
&mut self,
|
&mut self,
|
||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
list: WeakWidget<LazySpan>,
|
|
||||||
selection: Rc<RefCell<Selection>>,
|
|
||||||
key: RowKey,
|
|
||||||
sender: Option<&str>,
|
sender: Option<&str>,
|
||||||
markdown_src: &str,
|
markdown_src: &str,
|
||||||
) -> bool
|
) -> bool
|
||||||
@@ -461,15 +448,12 @@ impl RowBlocks {
|
|||||||
for (i, block) in new_blocks.iter().enumerate().skip(common) {
|
for (i, block) in new_blocks.iter().enumerate().skip(common) {
|
||||||
match (self.fields.get(i), self.links.get(i)) {
|
match (self.fields.get(i), self.links.get(i)) {
|
||||||
(Some(field), Some(links)) => {
|
(Some(field), Some(links)) => {
|
||||||
let rendered = render_block(block, BASE_SIZE);
|
let rendered = render_block(block, BASE_SIZE, &self.theme);
|
||||||
field
|
field(rsc).set_with_spans(rendered.text, rendered.spans);
|
||||||
.edit(rsc)
|
|
||||||
.set_with_spans(&rendered.text, rendered.spans);
|
|
||||||
*links.borrow_mut() = rendered.links;
|
*links.borrow_mut() = rendered.links;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let (field, framed, links) =
|
let (field, framed, links) = build_block(rsc, block, &self.theme);
|
||||||
build_block(rsc, list, selection.clone(), (key, i as u32), block);
|
|
||||||
self.fields.push(field);
|
self.fields.push(field);
|
||||||
self.links.push(links);
|
self.links.push(links);
|
||||||
if let Some(column) = rsc.ui_mut().widgets.get_mut(&self.column) {
|
if let Some(column) = rsc.ui_mut().widgets.get_mut(&self.column) {
|
||||||
@@ -486,16 +470,16 @@ impl RowBlocks {
|
|||||||
fn build_single<Rsc: HasEvents>(
|
fn build_single<Rsc: HasEvents>(
|
||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
list: WeakWidget<LazySpan>,
|
list: WeakWidget<LazySpan>,
|
||||||
selection: Rc<RefCell<Selection>>,
|
|
||||||
key: RowKey,
|
key: RowKey,
|
||||||
item: &TranscriptItem,
|
item: &TranscriptItem,
|
||||||
cap: bool,
|
cap: bool,
|
||||||
|
theme: Rc<Theme>,
|
||||||
) -> (StrongWidget, RowBlocks)
|
) -> (StrongWidget, RowBlocks)
|
||||||
where
|
where
|
||||||
Rsc::State: FocusHost + OpenUrl,
|
Rsc::State: FocusHost + OpenUrl,
|
||||||
{
|
{
|
||||||
let (sender, markdown_src) = item_content(item);
|
let (sender, markdown_src) = item_content(item);
|
||||||
build_text_row(rsc, list, selection, key, sender, &markdown_src, cap)
|
build_text_row(rsc, list, key, sender, &markdown_src, cap, theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Two mechanisms would have been two answers to the same question ("what
|
/// Two mechanisms would have been two answers to the same question ("what
|
||||||
@@ -515,10 +499,10 @@ pub enum TailRow {
|
|||||||
pub fn build_row<Rsc: HasEvents>(
|
pub fn build_row<Rsc: HasEvents>(
|
||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
list: WeakWidget<LazySpan>,
|
list: WeakWidget<LazySpan>,
|
||||||
selection: Rc<RefCell<Selection>>,
|
|
||||||
row: &FoldedRow,
|
row: &FoldedRow,
|
||||||
working: bool,
|
working: bool,
|
||||||
cap: bool,
|
cap: bool,
|
||||||
|
theme: Rc<Theme>,
|
||||||
) -> (RowKey, StrongWidget, Option<TailRow>)
|
) -> (RowKey, StrongWidget, Option<TailRow>)
|
||||||
where
|
where
|
||||||
Rsc::State: FocusHost + OpenUrl,
|
Rsc::State: FocusHost + OpenUrl,
|
||||||
@@ -537,14 +521,14 @@ where
|
|||||||
if let Some(calls) = calls {
|
if let Some(calls) = calls {
|
||||||
let key = row_key(&calls[0].key());
|
let key = row_key(&calls[0].key());
|
||||||
let (widget, tools) =
|
let (widget, tools) =
|
||||||
crate::ui::tool::build_tool_row(rsc, list, selection, key, calls.to_vec(), working);
|
crate::ui::tool::build_tool_row(rsc, list, key, calls.to_vec(), working, theme);
|
||||||
return (key, widget, Some(TailRow::Tools(tools)));
|
return (key, widget, Some(TailRow::Tools(tools)));
|
||||||
}
|
}
|
||||||
let FoldedRow::Single(item) = row else {
|
let FoldedRow::Single(item) = row else {
|
||||||
unreachable!("every Tools row took the branch above");
|
unreachable!("every Tools row took the branch above");
|
||||||
};
|
};
|
||||||
let key = row_key(&item.key());
|
let key = row_key(&item.key());
|
||||||
let (widget, blocks) = build_single(rsc, list, selection, key, item, cap);
|
let (widget, blocks) = build_single(rsc, list, key, item, cap, theme);
|
||||||
(key, widget, Some(TailRow::Blocks(blocks)))
|
(key, widget, Some(TailRow::Blocks(blocks)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,378 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
use std::{collections::BTreeMap, time::Instant};
|
|
||||||
|
|
||||||
pub type SelKey = (RowKey, u32);
|
|
||||||
|
|
||||||
pub struct Selection {
|
|
||||||
rows: BTreeMap<SelKey, WeakWidget<TextEdit>>,
|
|
||||||
anchor: Option<(SelKey, Vec2)>,
|
|
||||||
gesture: DragGesture,
|
|
||||||
scroll: Option<WeakWidget<LazySpan>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Selection {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Selection {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
rows: BTreeMap::new(),
|
|
||||||
anchor: None,
|
|
||||||
gesture: DragGesture::new(),
|
|
||||||
scroll: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_scroll_area(&mut self, scroll: WeakWidget<LazySpan>) {
|
|
||||||
self.scroll = Some(scroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A row's selectable text became visible/known. Every addition here
|
|
||||||
/// needs its removal (`unregister`, or `clear` for all of them at
|
|
||||||
/// once) -- called when `LazySpan` evicts the row (`pop_front`/
|
|
||||||
/// `pop_back`/`clear`), so this map never outgrows however many rows
|
|
||||||
/// are actually loaded. `LazySpan::place` guards the twin of this same
|
|
||||||
/// class of bug on the list's own side (`lazy_span.rs`'s `slot_exists`
|
|
||||||
/// assertion) -- a derived handle that silently outlives what it
|
|
||||||
/// points to; the next caller adding a third row-keyed side table
|
|
||||||
/// should read both.
|
|
||||||
pub fn register(&mut self, key: SelKey, text: WeakWidget<TextEdit>) {
|
|
||||||
self.rows.insert(key, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.rows.clear();
|
|
||||||
self.anchor = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unregister(&mut self, row: RowKey) {
|
|
||||||
self.rows.retain(|&(k, _), _| k != row);
|
|
||||||
if self.anchor.map(|((k, _), _)| k) == Some(row) {
|
|
||||||
self.anchor = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A fresh press: clears whatever was selected elsewhere (an ordinary
|
|
||||||
/// click starts a new selection, it does not extend the old one) and
|
|
||||||
/// gives `key`'s row a collapsed caret at `pos` -- a plain click that
|
|
||||||
/// never turns into a drag leaves exactly this and nothing else
|
|
||||||
/// selected.
|
|
||||||
pub fn begin(&mut self, ui: &mut impl UiRsc, key: SelKey, pos: Vec2, size: Vec2) {
|
|
||||||
let rows: Vec<SelKey> = self.rows.keys().copied().collect();
|
|
||||||
for k in rows {
|
|
||||||
if k != key
|
|
||||||
&& let Some(w) = self.rows.get(&k)
|
|
||||||
{
|
|
||||||
w.edit(ui).deselect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(w) = self.rows.get(&key) {
|
|
||||||
w.edit(ui).select(pos, size, false, false);
|
|
||||||
}
|
|
||||||
self.anchor = Some((key, pos));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extend(&mut self, ui: &mut impl UiRsc, key: SelKey, pos: Vec2, size: Vec2) {
|
|
||||||
let Some((anchor_key, _anchor_pos)) = self.anchor else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if key == anchor_key {
|
|
||||||
if let Some(w) = self.rows.get(&key) {
|
|
||||||
w.edit(ui).select(pos, size, true, false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let (lo, hi) = if anchor_key < key {
|
|
||||||
(anchor_key, key)
|
|
||||||
} else {
|
|
||||||
(key, anchor_key)
|
|
||||||
};
|
|
||||||
let in_range: Vec<SelKey> = self.rows.range(lo..=hi).map(|(&k, _)| k).collect();
|
|
||||||
for k in &in_range {
|
|
||||||
let Some(w) = self.rows.get(k).copied() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if *k == key {
|
|
||||||
let start = if key > anchor_key { Vec2::ZERO } else { size };
|
|
||||||
w.edit(ui).select(start, size, false, false);
|
|
||||||
w.edit(ui).select(pos, size, true, false);
|
|
||||||
} else {
|
|
||||||
w.edit(ui).select_all();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let outside: Vec<SelKey> = self
|
|
||||||
.rows
|
|
||||||
.keys()
|
|
||||||
.copied()
|
|
||||||
.filter(|k| *k < lo || *k > hi)
|
|
||||||
.collect();
|
|
||||||
for k in outside {
|
|
||||||
if let Some(w) = self.rows.get(&k) {
|
|
||||||
w.edit(ui).deselect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The block indices currently registered for `row`, in order. For a
|
|
||||||
/// test asserting that a row's removal or rebuild took every one of
|
|
||||||
/// its blocks with it -- the contract `unregister` states and the one
|
|
||||||
/// a caller can get wrong silently, since a stale handle only shows
|
|
||||||
/// up as a panic on some later, unrelated press.
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn registered_blocks(&self, row: RowKey) -> impl Iterator<Item = u32> + '_ {
|
|
||||||
self.rows
|
|
||||||
.keys()
|
|
||||||
.filter(move |(k, _)| *k == row)
|
|
||||||
.map(|&(_, b)| b)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn locate(
|
|
||||||
&self,
|
|
||||||
ui: &impl UiRsc,
|
|
||||||
render: &UiRenderState,
|
|
||||||
pos_window: Vec2,
|
|
||||||
) -> Option<(SelKey, Vec2, Vec2)> {
|
|
||||||
for (&key, w) in &self.rows {
|
|
||||||
let Some(px) = render.window_region(w, ui) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if px.contains(pos_window) {
|
|
||||||
return Some((key, pos_window - px.top_left, px.size()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_selection(&self, ui: &mut impl UiRsc) -> bool {
|
|
||||||
self.rows
|
|
||||||
.values()
|
|
||||||
.any(|w| w.edit(ui).text.selected_text().is_some())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `row`, if given, is `(key, pos_row, size)` for whichever row the
|
|
||||||
/// pointer is currently over -- row-local, as `begin`/`extend` want.
|
|
||||||
/// `None` once the gesture is pointer-captured (`iris::sense`'s
|
|
||||||
/// pointer-capture doc) and the current position falls outside every
|
|
||||||
/// row `LazySpan` has loaded (a gap, or off the end of the content); a
|
|
||||||
/// `Pan` outcome never needs it, so this only actually matters mid-
|
|
||||||
/// selection, where it is rare and the frame is simply dropped.
|
|
||||||
/// `pos_window` is in window space, since a pan's delta has to stay
|
|
||||||
/// meaningful even when this frame's event landed on a different row
|
|
||||||
/// than the last one. `pointer` is `CursorData`'s own field -- what
|
|
||||||
/// `DragGesture` needs to take pointer capture.
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
/// Returns what the gesture decided this frame, so a caller with its
|
|
||||||
/// own meaning for a *tap* -- a row's link handler -- reads it from
|
|
||||||
/// the one arbiter that already knows, rather than timing a second
|
|
||||||
/// one beside it (which would disagree the moment either changed).
|
|
||||||
pub fn drag(
|
|
||||||
&mut self,
|
|
||||||
ui: &mut impl UiRsc,
|
|
||||||
list: WeakWidget<LazySpan>,
|
|
||||||
row: Option<(SelKey, Vec2, Vec2)>,
|
|
||||||
pos_window: Vec2,
|
|
||||||
sense: CursorSense,
|
|
||||||
now: Instant,
|
|
||||||
pointer: &PointerRequests,
|
|
||||||
) -> GestureOutcome {
|
|
||||||
debug_assert!(
|
|
||||||
self.scroll.is_some(),
|
|
||||||
"Selection::drag with no scroll area: a committed pan would be silently dropped -- \
|
|
||||||
call `set_scroll_area` after building the transcript's `LazySpan`"
|
|
||||||
);
|
|
||||||
let mut press = PressState::default();
|
|
||||||
if self.gesture.starts_press(sense) {
|
|
||||||
press.scrolling = self.scroll.is_some_and(|s| s(ui).is_scrolling());
|
|
||||||
if let Some(scroll) = self.scroll {
|
|
||||||
scroll(ui).cancel_fling();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
press.already_selected = self.has_selection(ui);
|
|
||||||
let outcome = self
|
|
||||||
.gesture
|
|
||||||
.handle(pointer, list.id(), sense, pos_window, now, press);
|
|
||||||
match outcome {
|
|
||||||
// Somebody else took the gesture (a code fence panning
|
|
||||||
// sideways under the finger). Nothing here acted on it, and
|
|
||||||
// `DragGesture` has already forgotten it, so there is nothing
|
|
||||||
// to undo either -- the point is that no tap, fling or
|
|
||||||
// selection follows from a gesture that was never ours.
|
|
||||||
GestureOutcome::Cancelled | GestureOutcome::Undecided => {}
|
|
||||||
GestureOutcome::Pan(dy) => {
|
|
||||||
if let Some(scroll) = self.scroll {
|
|
||||||
scroll(ui).scroll(dy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GestureOutcome::SelectStart => {
|
|
||||||
if let Some((key, pos_row, size)) = row {
|
|
||||||
log::info!("iris selection: begin at row {key:?}");
|
|
||||||
self.begin(ui, key, pos_row, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GestureOutcome::SelectExtend => {
|
|
||||||
if let Some((key, pos_row, size)) = row {
|
|
||||||
log::info!("iris selection: extend to row {key:?}");
|
|
||||||
self.extend(ui, key, pos_row, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// A fling only ever follows a pan -- never a selection that
|
|
||||||
// happened to end with the finger still moving, and never a
|
|
||||||
// tap/long-press that never left `Undecided` -- exactly what
|
|
||||||
// `DragGesture`'s `Some(v)` already encodes.
|
|
||||||
GestureOutcome::Released(Some(v)) => {
|
|
||||||
// The half that actually makes it move -- see
|
|
||||||
// `ScrollController::fling`'s doc. Without it the velocity is
|
|
||||||
// computed, stored, and never advanced by anything.
|
|
||||||
if let Some(scroll) = self.scroll
|
|
||||||
&& scroll(ui).fling(v)
|
|
||||||
{
|
|
||||||
let id = scroll.id();
|
|
||||||
ui.ui_mut().animate(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GestureOutcome::Released(None) | GestureOutcome::Tapped => {}
|
|
||||||
}
|
|
||||||
outcome
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The concatenated selected text, in row order, `None` if nothing is
|
|
||||||
/// selected -- what a copy command reads. Joins with a blank line
|
|
||||||
/// between rows, matching how the transcript itself separates them.
|
|
||||||
pub fn selected_text(&self, ui: &mut impl UiRsc) -> Option<String> {
|
|
||||||
let mut parts = Vec::new();
|
|
||||||
for w in self.rows.values() {
|
|
||||||
if let Some(text) = w.edit(ui).text.selected_text() {
|
|
||||||
parts.push(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if parts.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(parts.join("\n\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn in_range(anchor: RowKey, current: RowKey, keys: &[RowKey]) -> Vec<RowKey> {
|
|
||||||
let (lo, hi) = if anchor < current {
|
|
||||||
(anchor, current)
|
|
||||||
} else {
|
|
||||||
(current, anchor)
|
|
||||||
};
|
|
||||||
keys.iter()
|
|
||||||
.copied()
|
|
||||||
.filter(|k| *k >= lo && *k <= hi)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn selection_spans_forward_across_rows() {
|
|
||||||
let keys = [1, 2, 3, 4, 5];
|
|
||||||
assert_eq!(in_range(2, 4, &keys), vec![2, 3, 4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn selection_spans_backward_across_rows() {
|
|
||||||
let keys = [1, 2, 3, 4, 5];
|
|
||||||
assert_eq!(in_range(4, 2, &keys), vec![2, 3, 4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn selection_within_one_row_is_just_that_row() {
|
|
||||||
let keys = [1, 2, 3];
|
|
||||||
assert_eq!(in_range(2, 2, &keys), vec![2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TestRsc {
|
|
||||||
ui: UiData,
|
|
||||||
}
|
|
||||||
impl UiRsc for TestRsc {
|
|
||||||
fn ui(&self) -> &UiData {
|
|
||||||
&self.ui
|
|
||||||
}
|
|
||||||
fn ui_mut(&mut self) -> &mut UiData {
|
|
||||||
&mut self.ui
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_missed_press_start_recovers_on_the_next_pressing_frame() {
|
|
||||||
let mut rsc = TestRsc {
|
|
||||||
ui: UiData::default(),
|
|
||||||
};
|
|
||||||
let field = rsc
|
|
||||||
.ui
|
|
||||||
.widgets
|
|
||||||
.add_strong(TextEdit::new(
|
|
||||||
TextView::new(TextBuffer::new_empty(), TextAttrs::default(), None),
|
|
||||||
EditMode::MultiLine,
|
|
||||||
))
|
|
||||||
.weak();
|
|
||||||
let list = rsc
|
|
||||||
.ui
|
|
||||||
.widgets
|
|
||||||
.add_strong(LazySpan::new(Dir::DOWN, Pin::End));
|
|
||||||
let list_weak = list.weak();
|
|
||||||
let scroll = rsc
|
|
||||||
.ui
|
|
||||||
.widgets
|
|
||||||
.add_strong(LazySpan::new(Dir::DOWN, Pin::End))
|
|
||||||
.weak();
|
|
||||||
let list = list_weak;
|
|
||||||
|
|
||||||
let mut sel = Selection::new();
|
|
||||||
sel.set_scroll_area(scroll);
|
|
||||||
sel.register((1, 0), field);
|
|
||||||
assert!(sel.gesture.is_idle());
|
|
||||||
|
|
||||||
let pointer = PointerRequests::default();
|
|
||||||
let now = Instant::now();
|
|
||||||
let size = Vec2::new(100.0, 20.0);
|
|
||||||
sel.drag(
|
|
||||||
&mut rsc,
|
|
||||||
list,
|
|
||||||
Some(((1, 0), Vec2::ZERO, size)),
|
|
||||||
Vec2::new(540.0, 700.0),
|
|
||||||
CursorSense::Pressing(CursorButton::Left),
|
|
||||||
now,
|
|
||||||
&pointer,
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!sel.gesture.is_idle(),
|
|
||||||
"a Pressing frame with the arbiter still Idle must recover \
|
|
||||||
the press rather than leaving it stuck"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unregister_forgets_every_block_of_the_row_and_clears_a_matching_anchor() {
|
|
||||||
let mut rsc = TestRsc {
|
|
||||||
ui: UiData::default(),
|
|
||||||
};
|
|
||||||
let field = rsc
|
|
||||||
.ui
|
|
||||||
.widgets
|
|
||||||
.add_strong(TextEdit::new(
|
|
||||||
TextView::new(TextBuffer::new_empty(), TextAttrs::default(), None),
|
|
||||||
EditMode::MultiLine,
|
|
||||||
))
|
|
||||||
.weak();
|
|
||||||
|
|
||||||
let mut sel = Selection::new();
|
|
||||||
sel.register((5, 0), field);
|
|
||||||
sel.register((5, 1), field);
|
|
||||||
sel.anchor = Some(((5, 1), Vec2::ZERO));
|
|
||||||
assert_eq!(sel.rows.len(), 2);
|
|
||||||
|
|
||||||
sel.unregister(5);
|
|
||||||
assert!(sel.rows.is_empty());
|
|
||||||
assert!(sel.anchor.is_none());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+8
-13
@@ -1,27 +1,22 @@
|
|||||||
use crate::ui::selection::Selection;
|
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use std::{cell::RefCell, rc::Rc};
|
|
||||||
|
|
||||||
pub(crate) fn on_tap<Rsc: HasEvents>(
|
pub(crate) fn on_tap<Rsc: HasEvents>(
|
||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
ptr: WeakWidget<WidgetPtr>,
|
ptr: WeakWidget<WidgetPtr>,
|
||||||
list: WeakWidget<LazySpan>,
|
list: WeakWidget<LazySpan>,
|
||||||
selection: Rc<RefCell<Selection>>,
|
|
||||||
f: impl Fn(&mut Rsc) + 'static,
|
f: impl Fn(&mut Rsc) + 'static,
|
||||||
) where
|
) where
|
||||||
Rsc::State: FocusHost + OpenUrl,
|
Rsc::State: FocusHost + OpenUrl,
|
||||||
{
|
{
|
||||||
ptr.on(CursorSense::drag_senses(), move |ctx, rsc| {
|
ptr.on(CursorSense::drag_senses(), move |ctx, rsc: &mut Rsc| {
|
||||||
let outcome = selection.borrow_mut().drag(
|
let input = &ctx.data;
|
||||||
rsc,
|
let outcome = rsc
|
||||||
|
.with_nearest_controller::<SelectionController, _>(
|
||||||
list,
|
list,
|
||||||
None,
|
|id, selection, rsc| selection.drag(id, rsc, input),
|
||||||
ctx.data.cursor.pos,
|
)
|
||||||
ctx.data.sense,
|
.unwrap_or(SelectionInput::Tapped);
|
||||||
ctx.data.cursor.time,
|
if outcome == SelectionInput::Tapped {
|
||||||
ctx.data.pointer,
|
|
||||||
);
|
|
||||||
if outcome == GestureOutcome::Tapped {
|
|
||||||
f(rsc);
|
f(rsc);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
/// The shared phone/desktop paint handles. Replacing their paint-table
|
||||||
|
/// entries changes the theme without rebuilding widgets or primitives.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Theme {
|
||||||
|
pub text: PaintId,
|
||||||
|
pub code: PaintId,
|
||||||
|
pub link: PaintId,
|
||||||
|
pub marker: PaintId,
|
||||||
|
pub verbatim_surface: PaintId,
|
||||||
|
pub table_surface: PaintId,
|
||||||
|
pub quote_bar: PaintId,
|
||||||
|
pub quote_text: PaintId,
|
||||||
|
pub strikethrough: PaintId,
|
||||||
|
pub card_surface: PaintId,
|
||||||
|
pub group_surface: PaintId,
|
||||||
|
pub muted: PaintId,
|
||||||
|
pub awaiting: PaintId,
|
||||||
|
pub failed: PaintId,
|
||||||
|
pub unknown: PaintId,
|
||||||
|
pub composer_surface: PaintId,
|
||||||
|
pub secondary_text: PaintId,
|
||||||
|
pub syntax_keyword: PaintId,
|
||||||
|
pub syntax_string: PaintId,
|
||||||
|
pub syntax_literal: PaintId,
|
||||||
|
pub syntax_comment: PaintId,
|
||||||
|
pub syntax_metadata: PaintId,
|
||||||
|
pub syntax_punctuation: PaintId,
|
||||||
|
pub syntax_mark: PaintId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Theme {
|
||||||
|
pub fn new(paints: &mut Paints) -> Self {
|
||||||
|
Self {
|
||||||
|
text: paints.add(srgb(0xCDD6F4)),
|
||||||
|
code: paints.add(srgb(0xCDD6F4)),
|
||||||
|
link: paints.add(srgb(0x89B4FA)),
|
||||||
|
marker: paints.add(srgb(0xB4BEFE)),
|
||||||
|
verbatim_surface: paints.add(srgb(0x11111B)),
|
||||||
|
table_surface: paints.add(srgb(0x313244)),
|
||||||
|
quote_bar: paints.add(srgb(0x585B70)),
|
||||||
|
quote_text: paints.add(srgb(0xA6ADC8)),
|
||||||
|
strikethrough: paints.add(srgb(0x6C7086)),
|
||||||
|
card_surface: paints.add(srgb(0x313244)),
|
||||||
|
group_surface: paints.add(srgb(0x181825)),
|
||||||
|
muted: paints.add(srgb(0xA6ADC8)),
|
||||||
|
awaiting: paints.add(srgb(0xFAB387)),
|
||||||
|
failed: paints.add(srgb(0xF38BA8)),
|
||||||
|
unknown: paints.add(srgb(0xF9E2AF)),
|
||||||
|
composer_surface: paints.add(Srgba8::rgb(40, 40, 46)),
|
||||||
|
secondary_text: paints.add(Srgba8::rgb(150, 150, 160)),
|
||||||
|
syntax_keyword: paints.add(srgb(0xCBA6F7)),
|
||||||
|
syntax_string: paints.add(srgb(0xA6E3A1)),
|
||||||
|
syntax_literal: paints.add(srgb(0xFAB387)),
|
||||||
|
syntax_comment: paints.add(srgb(0x6C7086)),
|
||||||
|
syntax_metadata: paints.add(srgb(0xF9E2AF)),
|
||||||
|
syntax_punctuation: paints.add(srgb(0xA6ADC8)),
|
||||||
|
syntax_mark: paints.add(srgb(0x89DCEB)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn srgb(hex: u32) -> Srgba8 {
|
||||||
|
Srgba8::rgb(
|
||||||
|
((hex >> 16) & 0xff) as u8,
|
||||||
|
((hex >> 8) & 0xff) as u8,
|
||||||
|
(hex & 0xff) as u8,
|
||||||
|
)
|
||||||
|
}
|
||||||
+72
-79
@@ -1,27 +1,12 @@
|
|||||||
use crate::client::text_cap::{VERBATIM_BYTES, VERBATIM_LINES, cut, show_all_label};
|
use crate::client::text_cap::{VERBATIM_BYTES, VERBATIM_LINES, cut, show_all_label};
|
||||||
use crate::client::tool_summary::{ToolInput, parse_tool_input};
|
use crate::client::tool_summary::{ToolInput, parse_tool_input};
|
||||||
use crate::client::transcript_fold::{ToolState, TranscriptItem};
|
use crate::client::transcript_fold::{ToolState, TranscriptItem};
|
||||||
use crate::ui::markdown::{TEXT_COLOR, VERBATIM_BACKGROUND, highlight_into};
|
use crate::ui::markdown::highlight_into;
|
||||||
use crate::ui::selection::Selection;
|
|
||||||
use crate::ui::tap::{hold_edge, on_tap};
|
use crate::ui::tap::{hold_edge, on_tap};
|
||||||
|
use crate::ui::theme::Theme;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use std::{cell::Cell, cell::RefCell, collections::HashMap, rc::Rc};
|
use std::{cell::Cell, cell::RefCell, collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
const CARD_FILL: UiColor = UiColor::new(0x31, 0x32, 0x44, 255);
|
|
||||||
/// The surface a group's cards sit on: Mantle, one step *below* the page.
|
|
||||||
/// That surface is the single cue saying these calls belong together, and
|
|
||||||
/// it goes below rather than above because the cards are already above --
|
|
||||||
/// two steps in the same direction render as one flat block.
|
|
||||||
const GROUP_FILL: UiColor = UiColor::new(0x18, 0x18, 0x25, 255);
|
|
||||||
const NAME_COLOR: UiColor = TEXT_COLOR;
|
|
||||||
const MUTED_COLOR: UiColor = UiColor::new(0xA6, 0xAD, 0xC8, 255);
|
|
||||||
|
|
||||||
/// Waiting on a person -- Peach, `Theme.kt`'s `awaitingColor`. The same
|
|
||||||
/// colour a question card takes, because it is the same fact.
|
|
||||||
const AWAITING_COLOR: UiColor = UiColor::new(0xFA, 0xB3, 0x87, 255);
|
|
||||||
const FAILED_COLOR: UiColor = UiColor::new(0xF3, 0x8B, 0xA8, 255);
|
|
||||||
const UNKNOWN_COLOR: UiColor = UiColor::new(0xF9, 0xE2, 0xAF, 255);
|
|
||||||
|
|
||||||
const NAME_SIZE: f32 = 14.0;
|
const NAME_SIZE: f32 = 14.0;
|
||||||
const BODY_SIZE: f32 = 12.0;
|
const BODY_SIZE: f32 = 12.0;
|
||||||
const LABEL_SIZE: f32 = 11.0;
|
const LABEL_SIZE: f32 = 11.0;
|
||||||
@@ -58,9 +43,9 @@ struct Shared {
|
|||||||
cards: RefCell<Vec<WeakWidget<WidgetPtr>>>,
|
cards: RefCell<Vec<WeakWidget<WidgetPtr>>>,
|
||||||
content: RefCell<Option<WeakWidget<WidgetPtr>>>,
|
content: RefCell<Option<WeakWidget<WidgetPtr>>>,
|
||||||
list: WeakWidget<LazySpan>,
|
list: WeakWidget<LazySpan>,
|
||||||
selection: Rc<RefCell<Selection>>,
|
|
||||||
key: RowKey,
|
key: RowKey,
|
||||||
working: Cell<bool>,
|
working: Cell<bool>,
|
||||||
|
theme: Rc<Theme>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One transcript row's worth of tool calls, kept by the caller for the
|
/// One transcript row's worth of tool calls, kept by the caller for the
|
||||||
@@ -71,18 +56,18 @@ pub struct ToolRow {
|
|||||||
shared: Rc<Shared>,
|
shared: Rc<Shared>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text<Rsc>(content: impl Into<String>, size: f32, color: UiColor) -> TextBuilder<Rsc> {
|
fn text<Rsc>(content: impl Into<String>, size: f32, color: PaintId) -> TextBuilder<Rsc> {
|
||||||
wtext(content)
|
wtext(content)
|
||||||
.size(size)
|
.size(size)
|
||||||
.color(color)
|
.color(color)
|
||||||
.text_align(Align::LEFT)
|
.text_align(Align::LEFT)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn disclosure<Rsc>(glyph: &'static str) -> TextBuilder<Rsc> {
|
fn disclosure<Rsc>(glyph: &'static str, theme: &Theme) -> TextBuilder<Rsc> {
|
||||||
text(glyph, MARK_DP, MUTED_COLOR).family(Family::Icons)
|
text(glyph, MARK_DP, theme.muted.clone()).family(Family::Icons)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn raw_block<Rsc: HasEvents>(rsc: &mut Rsc, body: TextBuilder<Rsc>) -> StrongWidget
|
fn raw_block<Rsc: HasEvents>(rsc: &mut Rsc, body: TextBuilder<Rsc>, theme: &Theme) -> StrongWidget
|
||||||
where
|
where
|
||||||
Rsc::State: FocusHost,
|
Rsc::State: FocusHost,
|
||||||
{
|
{
|
||||||
@@ -94,22 +79,36 @@ where
|
|||||||
field
|
field
|
||||||
.scrollable(Axis::X, Pin::Start)
|
.scrollable(Axis::X, Pin::Start)
|
||||||
.pad(dp(RAW_PAD_DP))
|
.pad(dp(RAW_PAD_DP))
|
||||||
.masked_by(rect(VERBATIM_BACKGROUND).radius(dp(RAW_RADIUS_DP)))
|
.masked_by(rect(theme.verbatim_surface.clone()).radius(dp(RAW_RADIUS_DP)))
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any()
|
.any()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state_mark(state: ToolState) -> Option<(&'static str, UiColor)> {
|
fn state_word(state: ToolState) -> Option<&'static str> {
|
||||||
match state {
|
match state {
|
||||||
ToolState::Deciding => Some(("your turn", AWAITING_COLOR)),
|
ToolState::Deciding => Some("your turn"),
|
||||||
ToolState::Running => Some(("running", MUTED_COLOR)),
|
ToolState::Running => Some("running"),
|
||||||
ToolState::Failed => Some(("failed", FAILED_COLOR)),
|
ToolState::Failed => Some("failed"),
|
||||||
ToolState::NoResult => Some(("no result", UNKNOWN_COLOR)),
|
ToolState::NoResult => Some("no result"),
|
||||||
ToolState::Succeeded => None,
|
ToolState::Succeeded => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn state_mark(state: ToolState, theme: &Theme) -> Option<(&'static str, PaintId)> {
|
||||||
|
let color = match state {
|
||||||
|
ToolState::Deciding => theme.awaiting.clone(),
|
||||||
|
ToolState::Running => theme.muted.clone(),
|
||||||
|
ToolState::Failed => theme.failed.clone(),
|
||||||
|
ToolState::NoResult => theme.unknown.clone(),
|
||||||
|
ToolState::Succeeded => return None,
|
||||||
|
};
|
||||||
|
Some((
|
||||||
|
state_word(state).expect("non-success state has a label"),
|
||||||
|
color,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// What a screen reader is given for one card, and what a `ui-trace`
|
/// What a screen reader is given for one card, and what a `ui-trace`
|
||||||
/// script taps by: the tool, what the call is for, and how it went when
|
/// script taps by: the tool, what the call is for, and how it went when
|
||||||
/// that is anything but "fine" -- the same three things the Compose card's
|
/// that is anything but "fine" -- the same three things the Compose card's
|
||||||
@@ -120,7 +119,7 @@ fn card_label(tool: &str, parsed: &ToolInput, state: ToolState) -> String {
|
|||||||
name.push_str(": ");
|
name.push_str(": ");
|
||||||
name.push_str(title);
|
name.push_str(title);
|
||||||
}
|
}
|
||||||
if let Some((word, _)) = state_mark(state) {
|
if let Some(word) = state_word(state) {
|
||||||
name.push_str(" (");
|
name.push_str(" (");
|
||||||
name.push_str(word);
|
name.push_str(word);
|
||||||
name.push(')');
|
name.push(')');
|
||||||
@@ -170,18 +169,13 @@ where
|
|||||||
let label = show_all_label(lines);
|
let label = show_all_label(lines);
|
||||||
let more_strong = WidgetPtr::new().add_strong(rsc);
|
let more_strong = WidgetPtr::new().add_strong(rsc);
|
||||||
let more = more_strong.weak();
|
let more = more_strong.weak();
|
||||||
let words = text(label.clone(), LABEL_SIZE, MUTED_COLOR)
|
let words = text(label.clone(), LABEL_SIZE, shared.theme.muted.clone())
|
||||||
.label(label)
|
.label(label)
|
||||||
.add_strong(rsc);
|
.add_strong(rsc);
|
||||||
more(rsc).set(words);
|
more(rsc).set(words);
|
||||||
let shared_for_tap = shared.clone();
|
let shared_for_tap = shared.clone();
|
||||||
let key = (id.to_string(), part);
|
let key = (id.to_string(), part);
|
||||||
on_tap(
|
on_tap(rsc, more, shared.list, move |rsc| {
|
||||||
rsc,
|
|
||||||
more,
|
|
||||||
shared.list,
|
|
||||||
shared.selection.clone(),
|
|
||||||
move |rsc| {
|
|
||||||
hold_edge(rsc, shared_for_tap.list, shared_for_tap.key);
|
hold_edge(rsc, shared_for_tap.list, shared_for_tap.key);
|
||||||
shared_for_tap
|
shared_for_tap
|
||||||
.state
|
.state
|
||||||
@@ -189,8 +183,7 @@ where
|
|||||||
.whole
|
.whole
|
||||||
.insert(key.clone(), true);
|
.insert(key.clone(), true);
|
||||||
redraw_card(rsc, &shared_for_tap, index);
|
redraw_card(rsc, &shared_for_tap, index);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
more_strong.any()
|
more_strong.any()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,19 +200,25 @@ where
|
|||||||
{
|
{
|
||||||
if output.is_empty() {
|
if output.is_empty() {
|
||||||
let (words, colour) = match call_state {
|
let (words, colour) = match call_state {
|
||||||
ToolState::Succeeded => ("No output", MUTED_COLOR),
|
ToolState::Succeeded => ("No output", shared.theme.muted.clone()),
|
||||||
ToolState::Failed => ("Failed, with no output", FAILED_COLOR),
|
ToolState::Failed => ("Failed, with no output", shared.theme.failed.clone()),
|
||||||
ToolState::NoResult => ("No result ever arrived", UNKNOWN_COLOR),
|
ToolState::NoResult => ("No result ever arrived", shared.theme.unknown.clone()),
|
||||||
ToolState::Running | ToolState::Deciding => ("No output yet", MUTED_COLOR),
|
ToolState::Running | ToolState::Deciding => {
|
||||||
|
("No output yet", shared.theme.muted.clone())
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return text(words, LABEL_SIZE, colour).add_strong(rsc).any();
|
return text(words, LABEL_SIZE, colour).add_strong(rsc).any();
|
||||||
}
|
}
|
||||||
|
|
||||||
let (shown, lines, was_cut) = capped(output, wants_whole(shared, id, Part::Output));
|
let (shown, lines, was_cut) = capped(output, wants_whole(shared, id, Part::Output));
|
||||||
let mut column = Span::empty(Dir::DOWN).gap(dp(2));
|
let mut column = Span::empty(Dir::DOWN).gap(dp(2));
|
||||||
column.push(text("Output", LABEL_SIZE, NAME_COLOR).add_strong(rsc).any());
|
column.push(
|
||||||
let body = text(shown.to_string(), BODY_SIZE, NAME_COLOR);
|
text("Output", LABEL_SIZE, shared.theme.text.clone())
|
||||||
column.push(raw_block(rsc, body));
|
.add_strong(rsc)
|
||||||
|
.any(),
|
||||||
|
);
|
||||||
|
let body = text(shown.to_string(), BODY_SIZE, shared.theme.text.clone());
|
||||||
|
column.push(raw_block(rsc, body, &shared.theme));
|
||||||
if was_cut {
|
if was_cut {
|
||||||
column.push(show_all(rsc, shared, index, id, Part::Output, lines));
|
column.push(show_all(rsc, shared, index, id, Part::Output, lines));
|
||||||
}
|
}
|
||||||
@@ -249,12 +248,12 @@ where
|
|||||||
|
|
||||||
let mut header = Span::empty(Dir::RIGHT).gap(dp(GAP_DP));
|
let mut header = Span::empty(Dir::RIGHT).gap(dp(GAP_DP));
|
||||||
header.push(
|
header.push(
|
||||||
disclosure(if open { icon::OPEN } else { icon::CLOSED })
|
disclosure(if open { icon::OPEN } else { icon::CLOSED }, &shared.theme)
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any(),
|
.any(),
|
||||||
);
|
);
|
||||||
header.push(
|
header.push(
|
||||||
text(tool.clone(), NAME_SIZE, NAME_COLOR)
|
text(tool.clone(), NAME_SIZE, shared.theme.text.clone())
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any(),
|
.any(),
|
||||||
);
|
);
|
||||||
@@ -263,7 +262,7 @@ where
|
|||||||
header.push(Span::empty(Dir::RIGHT).width(rest(1)).add_strong(rsc).any())
|
header.push(Span::empty(Dir::RIGHT).width(rest(1)).add_strong(rsc).any())
|
||||||
}
|
}
|
||||||
(false, Some(title)) => header.push(
|
(false, Some(title)) => header.push(
|
||||||
text(title.to_string(), BODY_SIZE, MUTED_COLOR)
|
text(title.to_string(), BODY_SIZE, shared.theme.muted.clone())
|
||||||
.wrap(false)
|
.wrap(false)
|
||||||
.masked()
|
.masked()
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
@@ -273,12 +272,16 @@ where
|
|||||||
}
|
}
|
||||||
if open && let Some(timeout) = &parsed.timeout {
|
if open && let Some(timeout) = &parsed.timeout {
|
||||||
header.push(
|
header.push(
|
||||||
text(format!("timeout {timeout}"), LABEL_SIZE, MUTED_COLOR)
|
text(
|
||||||
|
format!("timeout {timeout}"),
|
||||||
|
LABEL_SIZE,
|
||||||
|
shared.theme.muted.clone(),
|
||||||
|
)
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any(),
|
.any(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if let Some((word, colour)) = state_mark(call_state) {
|
if let Some((word, colour)) = state_mark(call_state, &shared.theme) {
|
||||||
header.push(text(word, LABEL_SIZE, colour).add_strong(rsc).any());
|
header.push(text(word, LABEL_SIZE, colour).add_strong(rsc).any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +290,7 @@ where
|
|||||||
if open {
|
if open {
|
||||||
if let Some(description) = &parsed.description {
|
if let Some(description) = &parsed.description {
|
||||||
column.push(
|
column.push(
|
||||||
text(description.clone(), BODY_SIZE, MUTED_COLOR)
|
text(description.clone(), BODY_SIZE, shared.theme.muted.clone())
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any(),
|
.any(),
|
||||||
@@ -303,13 +306,13 @@ where
|
|||||||
let spans = match parsed.language {
|
let spans = match parsed.language {
|
||||||
Some(language) => {
|
Some(language) => {
|
||||||
let mut spans = Vec::new();
|
let mut spans = Vec::new();
|
||||||
highlight_into(&mut spans, shown, 0..shown.len(), language);
|
highlight_into(&mut spans, shown, 0..shown.len(), language, &shared.theme);
|
||||||
spans
|
spans
|
||||||
}
|
}
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
};
|
};
|
||||||
let body = text(shown.to_string(), BODY_SIZE, NAME_COLOR).spans(spans);
|
let body = text(shown.to_string(), BODY_SIZE, shared.theme.text.clone()).spans(spans);
|
||||||
column.push(raw_block(rsc, body));
|
column.push(raw_block(rsc, body, &shared.theme));
|
||||||
}
|
}
|
||||||
if !parsed.rest.is_empty() {
|
if !parsed.rest.is_empty() {
|
||||||
// Never dropped: a field left out would be claiming the tool
|
// Never dropped: a field left out would be claiming the tool
|
||||||
@@ -320,8 +323,8 @@ where
|
|||||||
let (shown, lines, was_cut) = capped(&joined, whole);
|
let (shown, lines, was_cut) = capped(&joined, whole);
|
||||||
input_lines += lines;
|
input_lines += lines;
|
||||||
input_cut |= was_cut;
|
input_cut |= was_cut;
|
||||||
let body = text(shown.to_string(), BODY_SIZE, MUTED_COLOR);
|
let body = text(shown.to_string(), BODY_SIZE, shared.theme.muted.clone());
|
||||||
column.push(raw_block(rsc, body));
|
column.push(raw_block(rsc, body, &shared.theme));
|
||||||
}
|
}
|
||||||
if input_cut {
|
if input_cut {
|
||||||
column.push(show_all(rsc, shared, index, id, Part::Input, input_lines));
|
column.push(show_all(rsc, shared, index, id, Part::Input, input_lines));
|
||||||
@@ -332,7 +335,7 @@ where
|
|||||||
column
|
column
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
.pad(dp(CARD_PAD_DP))
|
.pad(dp(CARD_PAD_DP))
|
||||||
.background(rect(CARD_FILL).radius(dp(CARD_RADIUS_DP)))
|
.background(rect(shared.theme.card_surface.clone()).radius(dp(CARD_RADIUS_DP)))
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
.label(card_label(tool, &parsed, call_state))
|
.label(card_label(tool, &parsed, call_state))
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
@@ -370,12 +373,7 @@ where
|
|||||||
let content = build_card(rsc, shared, index);
|
let content = build_card(rsc, shared, index);
|
||||||
ptr(rsc).set(content);
|
ptr(rsc).set(content);
|
||||||
let for_tap = shared.clone();
|
let for_tap = shared.clone();
|
||||||
on_tap(
|
on_tap(rsc, ptr, shared.list, move |rsc| {
|
||||||
rsc,
|
|
||||||
ptr,
|
|
||||||
shared.list,
|
|
||||||
shared.selection.clone(),
|
|
||||||
move |rsc| {
|
|
||||||
hold_edge(rsc, for_tap.list, for_tap.key);
|
hold_edge(rsc, for_tap.list, for_tap.key);
|
||||||
let Some(id) = for_tap.call_id(index) else {
|
let Some(id) = for_tap.call_id(index) else {
|
||||||
debug_assert!(false, "tapped card {index} is no longer in the row");
|
debug_assert!(false, "tapped card {index} is no longer in the row");
|
||||||
@@ -390,8 +388,7 @@ where
|
|||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
for_tap.state.borrow_mut().open.insert(id, !was);
|
for_tap.state.borrow_mut().open.insert(id, !was);
|
||||||
redraw_card(rsc, &for_tap, index);
|
redraw_card(rsc, &for_tap, index);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
(strong.any(), ptr)
|
(strong.any(), ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +398,7 @@ where
|
|||||||
{
|
{
|
||||||
let strong = WidgetPtr::new().add_strong(rsc);
|
let strong = WidgetPtr::new().add_strong(rsc);
|
||||||
let ptr = strong.weak();
|
let ptr = strong.weak();
|
||||||
let mark = disclosure(icon::COLLAPSE)
|
let mark = disclosure(icon::COLLAPSE, &shared.theme)
|
||||||
.center_text()
|
.center_text()
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
.pad(dp(CARD_PAD_DP))
|
.pad(dp(CARD_PAD_DP))
|
||||||
@@ -411,13 +408,9 @@ where
|
|||||||
.add_strong(rsc);
|
.add_strong(rsc);
|
||||||
ptr(rsc).set(mark);
|
ptr(rsc).set(mark);
|
||||||
let for_tap = shared.clone();
|
let for_tap = shared.clone();
|
||||||
on_tap(
|
on_tap(rsc, ptr, shared.list, move |rsc| {
|
||||||
rsc,
|
toggle_group(rsc, &for_tap)
|
||||||
ptr,
|
});
|
||||||
shared.list,
|
|
||||||
shared.selection.clone(),
|
|
||||||
move |rsc| toggle_group(rsc, &for_tap),
|
|
||||||
);
|
|
||||||
strong.any()
|
strong.any()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,10 +431,10 @@ where
|
|||||||
|
|
||||||
if !shared.state.borrow().group_expanded {
|
if !shared.state.borrow().group_expanded {
|
||||||
let heading = group_label(count);
|
let heading = group_label(count);
|
||||||
return text(heading.clone(), NAME_SIZE, NAME_COLOR)
|
return text(heading.clone(), NAME_SIZE, shared.theme.text.clone())
|
||||||
.pad(dp(CARD_PAD_DP))
|
.pad(dp(CARD_PAD_DP))
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
.background(rect(CARD_FILL).radius(dp(CARD_RADIUS_DP)))
|
.background(rect(shared.theme.card_surface.clone()).radius(dp(CARD_RADIUS_DP)))
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
.label(heading)
|
.label(heading)
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
@@ -451,7 +444,7 @@ where
|
|||||||
let heading = group_label(count);
|
let heading = group_label(count);
|
||||||
let mut group = Span::empty(Dir::DOWN);
|
let mut group = Span::empty(Dir::DOWN);
|
||||||
group.push(
|
group.push(
|
||||||
text(heading.clone(), NAME_SIZE, NAME_COLOR)
|
text(heading.clone(), NAME_SIZE, shared.theme.text.clone())
|
||||||
.pad(dp(CARD_PAD_DP))
|
.pad(dp(CARD_PAD_DP))
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
.label(heading)
|
.label(heading)
|
||||||
@@ -468,7 +461,7 @@ where
|
|||||||
group.push(collapse_bar(rsc, shared));
|
group.push(collapse_bar(rsc, shared));
|
||||||
group
|
group
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
.background(rect(GROUP_FILL).radius(dp(CARD_RADIUS_DP)))
|
.background(rect(shared.theme.group_surface.clone()).radius(dp(CARD_RADIUS_DP)))
|
||||||
.width(rest(1))
|
.width(rest(1))
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any()
|
.any()
|
||||||
@@ -516,10 +509,10 @@ impl Shared {
|
|||||||
pub fn build_tool_row<Rsc: HasEvents>(
|
pub fn build_tool_row<Rsc: HasEvents>(
|
||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
list: WeakWidget<LazySpan>,
|
list: WeakWidget<LazySpan>,
|
||||||
selection: Rc<RefCell<Selection>>,
|
|
||||||
key: RowKey,
|
key: RowKey,
|
||||||
calls: Vec<TranscriptItem>,
|
calls: Vec<TranscriptItem>,
|
||||||
working: bool,
|
working: bool,
|
||||||
|
theme: Rc<Theme>,
|
||||||
) -> (StrongWidget, ToolRow)
|
) -> (StrongWidget, ToolRow)
|
||||||
where
|
where
|
||||||
Rsc::State: FocusHost + OpenUrl,
|
Rsc::State: FocusHost + OpenUrl,
|
||||||
@@ -530,9 +523,9 @@ where
|
|||||||
cards: RefCell::new(Vec::new()),
|
cards: RefCell::new(Vec::new()),
|
||||||
content: RefCell::new(None),
|
content: RefCell::new(None),
|
||||||
list,
|
list,
|
||||||
selection,
|
|
||||||
key,
|
key,
|
||||||
working: Cell::new(working),
|
working: Cell::new(working),
|
||||||
|
theme,
|
||||||
});
|
});
|
||||||
let content_strong = WidgetPtr::new().add_strong(rsc);
|
let content_strong = WidgetPtr::new().add_strong(rsc);
|
||||||
let content = content_strong.weak();
|
let content = content_strong.weak();
|
||||||
|
|||||||
@@ -127,6 +127,32 @@ fn a_long_press_and_drag_selects_text() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_tap_after_selection_deselects_text() {
|
||||||
|
let (mut h, screen) = opened();
|
||||||
|
h.replay(&script(
|
||||||
|
"select-then-tap",
|
||||||
|
"0 down 300 1000\n\
|
||||||
|
520 move 300 1000\n\
|
||||||
|
560 move 700 1000\n\
|
||||||
|
600 move 900 1000\n\
|
||||||
|
640 up 900 1000\n\
|
||||||
|
800 down 540 1000\n\
|
||||||
|
880 up 540 1000",
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
screen.selected_text(&mut h.rsc),
|
||||||
|
None,
|
||||||
|
"an ordinary tap after a selection must dismiss it"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
h.state.opened_urls,
|
||||||
|
Vec::<String>::new(),
|
||||||
|
"the deselecting tap must be consumed rather than activating content"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_composer_sits_above_a_simulated_ime_inset() {
|
fn the_composer_sits_above_a_simulated_ime_inset() {
|
||||||
let (mut h, screen) = opened();
|
let (mut h, screen) = opened();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use iris::harness::Harness;
|
|||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
|
|
||||||
const HEADER_H: f32 = 300.0;
|
const HEADER_H: f32 = 300.0;
|
||||||
const HEADER: UiColor = UiColor::new(28, 28, 34, 255);
|
const HEADER: Srgba8 = Srgba8::new(28, 28, 34, 255);
|
||||||
|
|
||||||
fn opened() -> (Harness, ai_app::ui::TranscriptScreen) {
|
fn opened() -> (Harness, ai_app::ui::TranscriptScreen) {
|
||||||
let mut h = Harness::new(phone_size(), PHONE_SCALE);
|
let mut h = Harness::new(phone_size(), PHONE_SCALE);
|
||||||
|
|||||||
+13
-30
@@ -2,35 +2,10 @@
|
|||||||
|
|
||||||
Only open Iris framework work lives here. Delete an item when it lands.
|
Only open Iris framework work lives here. Delete an item when it lands.
|
||||||
|
|
||||||
## Fix
|
|
||||||
|
|
||||||
- [ ] **Colours are not in a defined colour space. Fix this before a
|
|
||||||
styling pass.** Both render backends prefer an sRGB surface
|
|
||||||
(`default/render.rs` and `android/render.rs`), while `fs_main` returns
|
|
||||||
`unpack4x8unorm` palette and image bytes unchanged. An sRGB attachment
|
|
||||||
treats those values as linear and encodes them again: Mocha Crust
|
|
||||||
(17,17,27) became (73,73,91) in a desktop screenshot measured on
|
|
||||||
2026-09-06.
|
|
||||||
|
|
||||||
Neither diagnostics nor startup logging records Android's selected surface
|
|
||||||
format. A device exposing only a non-sRGB surface can hide the shared bug.
|
|
||||||
|
|
||||||
Done means defining one convention for palette bytes, decoded images,
|
|
||||||
colour emoji and the clear colour, then converting exactly once for the
|
|
||||||
selected target. Record the selected format in diagnostics, and add a GPU
|
|
||||||
test that draws known non-black, non-white pixels into an sRGB target and
|
|
||||||
reads the stored bytes back; screenshots from desktop and Android then
|
|
||||||
confirm the same Catppuccin values rather than serving as the definition.
|
|
||||||
|
|
||||||
## Build (for the port)
|
## Build (for the port)
|
||||||
|
|
||||||
Framework capabilities needed by `RUST.md`'s port plan:
|
Framework capabilities needed by `RUST.md`'s port plan:
|
||||||
|
|
||||||
- [ ] **Selectable, read-only text.** P0's report and P1's transcript rows
|
|
||||||
use `TextEdit` because `Selectable` is implemented only for it. That
|
|
||||||
makes prose focusable and opens the IME over text that cannot be edited.
|
|
||||||
A display widget needs the same selection geometry and clipboard path
|
|
||||||
without a text-input accessibility role or keyboard focus.
|
|
||||||
- [ ] **Overflow ellipsis with an explicit retained end.** `TextAttrs` can
|
- [ ] **Overflow ellipsis with an explicit retained end.** `TextAttrs` can
|
||||||
only wrap or clip, so a tool summary is cut with no mark. Parley has no
|
only wrap or clip, so a tool summary is cut with no mark. Parley has no
|
||||||
ellipsis primitive; use its line breaker to find the cut, but keep source
|
ellipsis primitive; use its line breaker to find the cut, but keep source
|
||||||
@@ -52,9 +27,9 @@ Framework capabilities needed by `RUST.md`'s port plan:
|
|||||||
- [ ] **Per-range backgrounds for rich text.** (**P1**.) Inline code is
|
- [ ] **Per-range backgrounds for rich text.** (**P1**.) Inline code is
|
||||||
already monospace and coloured, but matching Compose's chip also needs
|
already monospace and coloured, but matching Compose's chip also needs
|
||||||
the glyph run's boxes so a surface can be drawn behind exactly that byte
|
the glyph run's boxes so a surface can be drawn behind exactly that byte
|
||||||
range. `TextEdit` already computes the same geometry internally for its
|
range. The shared `TextSelection` engine already computes the same geometry
|
||||||
selection highlight; expose one shared primitive rather than giving the
|
for selection highlights; expose one shared primitive rather than giving
|
||||||
app a second text-layout path.
|
the app a second text-layout path.
|
||||||
- [ ] **A modal/dialog primitive.** (**P1**, reused by **P3** and
|
- [ ] **A modal/dialog primitive.** (**P1**, reused by **P3** and
|
||||||
**P5**.) Needed for the session settings dialog, `UsageDialog`'s
|
**P5**.) Needed for the session settings dialog, `UsageDialog`'s
|
||||||
equivalent, and the delete-with-`deleteForeign` confirmation with its
|
equivalent, and the delete-with-`deleteForeign` confirmation with its
|
||||||
@@ -69,6 +44,14 @@ Framework capabilities needed by `RUST.md`'s port plan:
|
|||||||
|
|
||||||
## Later
|
## Later
|
||||||
|
|
||||||
|
- [ ] **Intern independently constructed solid paint definitions.** Inline
|
||||||
|
`rect(Srgba8::...)` values currently receive a new `PaintId` each time.
|
||||||
|
Cache them by canonical linear RGBA bits, but keep `Paints::add` explicitly
|
||||||
|
unique so two semantic theme roles that start with the same value can later
|
||||||
|
change independently. Cache entries must be weak and disappear when the
|
||||||
|
last real handle releases the slot; gradients and texture paints need their
|
||||||
|
own identity rules rather than inheriting solid-value interning blindly.
|
||||||
|
|
||||||
- [ ] **Property/content animations.** Cosmetic, so after correctness and
|
- [ ] **Property/content animations.** Cosmetic, so after correctness and
|
||||||
parity. Keep them modular, like input; scrolling already animates through
|
parity. Keep them modular, like input; scrolling already animates through
|
||||||
`Widget::tick` and `UiData::animate`. A widget that does not opt in must
|
`Widget::tick` and `UiData::animate`. A widget that does not opt in must
|
||||||
@@ -85,8 +68,8 @@ Framework capabilities needed by `RUST.md`'s port plan:
|
|||||||
against `.masked_by(rect(BAR_FILL))` on the composer at the phone's own
|
against `.masked_by(rect(BAR_FILL))` on the composer at the phone's own
|
||||||
size and density and the two are identical to the pixel. What the pair
|
size and density and the two are identical to the pixel. What the pair
|
||||||
cannot express is a clip that is not a box: `Painter::set_mask` writes
|
cannot express is a clip that is not a box: `Painter::set_mask` writes
|
||||||
a `RectPrimitive::color(Color::NONE)` at the widget's own region, with
|
a `RectPrimitive::color` using `PaintId::NONE` at the widget's own region,
|
||||||
no radius, so `.background(rect(fill).radius(r)).masked()` draws a
|
with no radius, so `.background(rect(fill).radius(r)).masked()` draws a
|
||||||
rounded panel and then cuts its content square. Both other call sites
|
rounded panel and then cuts its content square. Both other call sites
|
||||||
(`row.rs`'s fence, `tool.rs`'s raw output) are rounded, which is why
|
(`row.rs`'s fence, `tool.rs`'s raw output) are rounded, which is why
|
||||||
the method stands for now.
|
the method stands for now.
|
||||||
|
|||||||
+11
-25
@@ -22,8 +22,7 @@ the command and measured value when evidence matters.
|
|||||||
next session should start. Its box below has the state.
|
next session should start. Its box below has the state.
|
||||||
- The port is one crate under `app-rust/`; `iris/` is only the UI framework.
|
- The port is one crate under `app-rust/`; `iris/` is only the UI framework.
|
||||||
- **Open across the rest of the docs**: `docs/IRIS_TODO.md` is iris's own
|
- **Open across the rest of the docs**: `docs/IRIS_TODO.md` is iris's own
|
||||||
list (colour-space correctness is the live one), `docs/TODO.md` is the
|
list; `docs/TODO.md` is the Compose app's.
|
||||||
Compose app's.
|
|
||||||
|
|
||||||
## Desktop and phone share the code
|
## Desktop and phone share the code
|
||||||
|
|
||||||
@@ -81,7 +80,7 @@ runs inside `cargo test`.
|
|||||||
pan, and (e) the composer clears a simulated 1000px IME inset
|
pan, and (e) the composer clears a simulated 1000px IME inset
|
||||||
(`Composer::set_bottom_inset`). Each was confirmed to fail without
|
(`Composer::set_bottom_inset`). Each was confirmed to fail without
|
||||||
its subject rather than assumed: dropping `animate(id)` from
|
its subject rather than assumed: dropping `animate(id)` from
|
||||||
`Selection::drag` -- the phone's own "fling does nothing" defect --
|
`SelectionController::drag` -- the phone's own "fling does nothing" defect --
|
||||||
and starting the fling curve at the wall clock each fail only the
|
and starting the fling curve at the wall clock each fail only the
|
||||||
flick test; flinging on `Tapped` fails only the tap test; a 5s
|
flick test; flinging on `Tapped` fails only the tap test; a 5s
|
||||||
`LONG_PRESS` fails only the selection test; a `set_bottom_inset` that
|
`LONG_PRESS` fails only the selection test; a `set_bottom_inset` that
|
||||||
@@ -269,11 +268,14 @@ already got right (v2 signing, `uses-feature`, ABI splits).
|
|||||||
Reading the Compose code for what a replacement must be able to express,
|
Reading the Compose code for what a replacement must be able to express,
|
||||||
rather than what it happens to look like:
|
rather than what it happens to look like:
|
||||||
|
|
||||||
1. **The transcript is one selectable body of text.** One
|
1. **The transcript is one selectable body of text.** A
|
||||||
`SelectionContainer` around the whole lazy list, so a selection runs from
|
`SelectionController` is registered directly on the lazy list, with no
|
||||||
a reply into the tool output beneath it. The framework needs selectable
|
selection widget in the layout tree, so a selection runs from a reply into
|
||||||
read-only rich text across many rows, with the platform's selection
|
the tool output beneath it. Each parent supplies either draw order or one
|
||||||
handles and clipboard on the phone.
|
visual axis for its immediate children; ordering is resolved only when
|
||||||
|
selection queries it. Iris owns the selection handles because its text is
|
||||||
|
drawn into one surface, while the platform supplies the clipboard and
|
||||||
|
related system services.
|
||||||
2. **Rich inline text**: markdown with links (one tap detector per text,
|
2. **Rich inline text**: markdown with links (one tap detector per text,
|
||||||
not a node per link), inline code chips drawn behind the text, tables
|
not a node per link), inline code chips drawn behind the text, tables
|
||||||
with wrapping cells and a sideways scroll, syntax-highlighted fences,
|
with wrapping cells and a sideways scroll, syntax-highlighted fences,
|
||||||
@@ -442,7 +444,7 @@ buffer on **every keystroke**. So there is nothing to adopt, and adding
|
|||||||
it would be upstream work in parley.
|
it would be upstream work in parley.
|
||||||
|
|
||||||
**And the app already does the thing incremental layout would buy.**
|
**And the app already does the thing incremental layout would buy.**
|
||||||
`RowBlocks::apply_delta` keeps one `TextEdit` per top-level markdown
|
`RowBlocks::apply_delta` keeps one `Text` per top-level markdown
|
||||||
block and re-shapes only the block a delta landed in; re-splitting the
|
block and re-shapes only the block a delta landed in; re-splitting the
|
||||||
markdown to find that block is 18µs at 18,000 characters and comparing
|
markdown to find that block is 18µs at 18,000 characters and comparing
|
||||||
the blocks is 470ns. Neither is the cost.
|
the blocks is 470ns. Neither is the cost.
|
||||||
@@ -664,12 +666,6 @@ pane is neither masked nor scrollable despite its construction comment saying
|
|||||||
it is both. This is an `app-rust` defect, not an iris framework item.
|
it is both. This is an `app-rust` defect, not an iris framework item.
|
||||||
|
|
||||||
- [ ] **P1 — session screen parity.** Continue in this order:
|
- [ ] **P1 — session screen parity.** Continue in this order:
|
||||||
- [ ] **Before the next parity slice — make iris's colour pipeline
|
|
||||||
correct.** Both backends currently prefer an sRGB surface while
|
|
||||||
the shader returns palette/image bytes as
|
|
||||||
linear values; `IRIS_TODO.md` has the measured mismatch and
|
|
||||||
pass condition. Do this before judging or centralising the
|
|
||||||
app's styling. It is correctness, not cosmetic polish.
|
|
||||||
- [ ] **P1c — history paging and jump-to-latest.** Wire
|
- [ ] **P1c — history paging and jump-to-latest.** Wire
|
||||||
`client::transcript_source` into `src/ui`:
|
`client::transcript_source` into `src/ui`:
|
||||||
the opening page, paging back on scroll with the cushion
|
the opening page, paging back on scroll with the cushion
|
||||||
@@ -725,16 +721,6 @@ it is both. This is an `app-rust` defect, not an iris framework item.
|
|||||||
names are app content applied through iris's existing `.label()` API,
|
names are app content applied through iris's existing `.label()` API,
|
||||||
not a missing framework widget.
|
not a missing framework widget.
|
||||||
|
|
||||||
**`app-rust` UI defect still open**: tool-card text is not selectable.
|
|
||||||
A `TranscriptRow::Tools` has no markdown blocks, so cards can number
|
|
||||||
their own texts from zero without colliding with the row selection
|
|
||||||
keys. The risk is lifecycle: every card rebuild path must unregister
|
|
||||||
the old `TextEdit` handles before registering replacements, or the next
|
|
||||||
long press can find a freed handle. Cover result arrival, group toggle,
|
|
||||||
a call joining a run and the per-card widget swap with stale-handle
|
|
||||||
tests. This uses iris's existing selection API; it is not a framework
|
|
||||||
widget gap.
|
|
||||||
|
|
||||||
**Pass condition**: `app/ui-sandbox.sh`'s fixtures driven by
|
**Pass condition**: `app/ui-sandbox.sh`'s fixtures driven by
|
||||||
`ui-trace record --do "tap '<label>'"` — a session with the big
|
`ui-trace record --do "tap '<label>'"` — a session with the big
|
||||||
transcript (`AI_SANDBOX_BIG_MB`), a paused/slow-spawning one
|
transcript (`AI_SANDBOX_BIG_MB`), a paused/slow-spawning one
|
||||||
|
|||||||
+5
-5
@@ -240,16 +240,16 @@ without a scroll-widget special case.
|
|||||||
The transcript registers the wheel **by hand rather than calling
|
The transcript registers the wheel **by hand rather than calling
|
||||||
`LazySpan::scrollable()`**, and this is not an oversight. That helper also
|
`LazySpan::scrollable()`**, and this is not an oversight. That helper also
|
||||||
registers a finger drag driving the span's own `DragGesture`, and the
|
registers a finger drag driving the span's own `DragGesture`, and the
|
||||||
transcript already has an arbiter — `Selection`, which must decide between
|
transcript already has an arbiter — `SelectionController`, which must decide between
|
||||||
panning and selecting text and so cannot let a second `DragGesture` see
|
panning and selecting text and so cannot let a second `DragGesture` see
|
||||||
the same frames. `DragGesture`'s doc states the rule: one gesture, one
|
the same frames. `DragGesture`'s doc states the rule: one gesture, one
|
||||||
arbiter, each frame delivered exactly once. The wheel handler registered
|
arbiter, each frame delivered exactly once. The wheel handler registered
|
||||||
here is identical to the helper's; only the drag differs.
|
here is identical to the helper's; only the drag differs.
|
||||||
|
|
||||||
`Selection` is given the span by `set_scroll_area` after it exists (rows
|
`SelectionController` is attached directly to the span and holds its weak
|
||||||
need a `Selection`, and the span needs the rows), and hands it committed
|
handle, then hands committed pans and releases through
|
||||||
pans and releases through `Scrollable::scroll`/`fling`. There is no
|
`Scrollable::scroll`/`fling`. There is no wrapper widget:
|
||||||
wrapper widget: `TranscriptScreen::list` is the layout (`extent`,
|
`TranscriptScreen::list` is the layout (`extent`,
|
||||||
`key_at`, `jump_to_end`) *and* the position (`amt`, `fling`,
|
`key_at`, `jump_to_end`) *and* the position (`amt`, `fling`,
|
||||||
`is_scrolling`).
|
`is_scrolling`).
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,26 @@ adjacency after a free.
|
|||||||
Standalone images currently use `NonFiltering` sampling. Thumbnail scaling
|
Standalone images currently use `NonFiltering` sampling. Thumbnail scaling
|
||||||
and filtering remain image-widget decisions, not texture-storage decisions.
|
and filtering remain image-widget decisions, not texture-storage decisions.
|
||||||
|
|
||||||
|
## Colour convention
|
||||||
|
|
||||||
|
Palette literals and decoded image bytes enter Iris as straight-alpha sRGB.
|
||||||
|
Solid paints are converted once when registered and stored in a separate GPU
|
||||||
|
paint table as linear `vec4<f32>`; rect and glyph primitives carry only the
|
||||||
|
paint-table index. `Paints::set` rewrites a slot in place, so a shared theme
|
||||||
|
can change without rebuilding widgets or primitive buffers. A rect may also
|
||||||
|
hold a `Paint` definition and resolve it to a `PaintId` on its first draw;
|
||||||
|
independently constructed definitions deliberately receive independent slots.
|
||||||
|
|
||||||
|
Standalone images, colour glyphs, and atlas pages use
|
||||||
|
`Rgba8UnormSrgb`, which makes sampling decode their RGB channels to linear
|
||||||
|
light. Shaders therefore always return linear values. Both window backends
|
||||||
|
render through an sRGB texture view in `SurfaceColorSpace::Srgb`, which
|
||||||
|
encodes exactly once on output; the clear colour is linear too. A surface
|
||||||
|
without an advertised RGBA/BGRA sRGB view and sRGB output space is rejected
|
||||||
|
rather than silently displaying a different colour pipeline. The readback
|
||||||
|
test in `iris/tests/color_space.rs` guards the solid, image, and in-place
|
||||||
|
theme-update paths end to end.
|
||||||
|
|
||||||
## Verification rig
|
## Verification rig
|
||||||
|
|
||||||
`scripts/rigs/gpu-probe` requests Iris's exact feature and limit set without a
|
`scripts/rigs/gpu-probe` requests Iris's exact feature and limit set without a
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ fn bench_input_grows(n: usize, lines: usize) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let line_height = 24.0;
|
let line_height = 24.0;
|
||||||
let input_rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let input_rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let input_area = rsc.ui.widgets.add_strong(Sized {
|
let input_area = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: input_rect.any(),
|
inner: input_rect.any(),
|
||||||
x: None,
|
x: None,
|
||||||
@@ -238,7 +238,7 @@ fn bench_expand_holds_edge(n: usize, growths: usize) {
|
|||||||
let mut growable = None;
|
let mut growable = None;
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
if i == growable_index {
|
if i == growable_index {
|
||||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let sized = rsc.ui.widgets.add_strong(Sized {
|
let sized = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: rect.any(),
|
inner: rect.any(),
|
||||||
x: None,
|
x: None,
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
use crate::{
|
||||||
|
ActiveData, WidgetId,
|
||||||
|
util::{HashMap, HashSet},
|
||||||
|
};
|
||||||
|
use std::any::{Any, TypeId};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||||
|
pub struct ControllerId {
|
||||||
|
host: WidgetId,
|
||||||
|
kind: TypeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControllerId {
|
||||||
|
pub fn host(self) -> WidgetId {
|
||||||
|
self.host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Command {
|
||||||
|
Copy,
|
||||||
|
SelectAll,
|
||||||
|
Escape,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum CommandResult {
|
||||||
|
Unused,
|
||||||
|
Used,
|
||||||
|
Copy(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ControllerValue: Any {
|
||||||
|
fn into_any(self: Box<Self>) -> Box<dyn Any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Any> ControllerValue for T {
|
||||||
|
fn into_any(self: Box<Self>) -> Box<dyn Any> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Controller<Rsc>: ControllerValue {
|
||||||
|
fn command(&mut self, _command: Command, _rsc: &mut Rsc) -> CommandResult {
|
||||||
|
CommandResult::Unused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ControllerManager<Rsc> {
|
||||||
|
by_widget: HashMap<WidgetId, HashMap<TypeId, Box<dyn Controller<Rsc>>>>,
|
||||||
|
parents: HashMap<WidgetId, Option<WidgetId>>,
|
||||||
|
borrowed: HashSet<ControllerId>,
|
||||||
|
removed_while_borrowed: HashSet<WidgetId>,
|
||||||
|
command_target: Option<ControllerId>,
|
||||||
|
command_target_revision: u64,
|
||||||
|
command_boundary: Option<WidgetId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Rsc> Default for ControllerManager<Rsc> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
by_widget: Default::default(),
|
||||||
|
parents: Default::default(),
|
||||||
|
borrowed: Default::default(),
|
||||||
|
removed_while_borrowed: Default::default(),
|
||||||
|
command_target: None,
|
||||||
|
command_target_revision: 0,
|
||||||
|
command_boundary: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Rsc: 'static> ControllerManager<Rsc> {
|
||||||
|
#[track_caller]
|
||||||
|
pub fn register<C: Controller<Rsc>>(&mut self, host: WidgetId, controller: C) {
|
||||||
|
let kind = TypeId::of::<C>();
|
||||||
|
let id = ControllerId { host, kind };
|
||||||
|
assert!(
|
||||||
|
!self.borrowed.contains(&id),
|
||||||
|
"a controller cannot be replaced while it is handling input"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!self.removed_while_borrowed.contains(&host),
|
||||||
|
"a controller cannot be attached to a removed widget"
|
||||||
|
);
|
||||||
|
let old = self
|
||||||
|
.by_widget
|
||||||
|
.entry(host)
|
||||||
|
.or_default()
|
||||||
|
.insert(kind, Box::new(controller));
|
||||||
|
assert!(
|
||||||
|
old.is_none(),
|
||||||
|
"a widget cannot have two controllers of type {}",
|
||||||
|
std::any::type_name::<C>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id<C: Controller<Rsc>>(&self, host: WidgetId) -> Option<ControllerId> {
|
||||||
|
let kind = TypeId::of::<C>();
|
||||||
|
self.by_widget
|
||||||
|
.get(&host)?
|
||||||
|
.contains_key(&kind)
|
||||||
|
.then_some(ControllerId { host, kind })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nearest_id<C: Controller<Rsc>>(&self, mut origin: WidgetId) -> Option<ControllerId> {
|
||||||
|
let kind = TypeId::of::<C>();
|
||||||
|
loop {
|
||||||
|
let candidate = ControllerId { host: origin, kind };
|
||||||
|
assert!(
|
||||||
|
!self.borrowed.contains(&candidate),
|
||||||
|
"a controller cannot re-enter itself while it is handling input"
|
||||||
|
);
|
||||||
|
if let Some(id) = self.id::<C>(origin) {
|
||||||
|
return Some(id);
|
||||||
|
}
|
||||||
|
origin = self.parents.get(&origin).copied().flatten()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path_to<C: Controller<Rsc>>(
|
||||||
|
&self,
|
||||||
|
mut origin: WidgetId,
|
||||||
|
) -> Option<(ControllerId, Vec<WidgetId>)> {
|
||||||
|
let mut path = Vec::new();
|
||||||
|
loop {
|
||||||
|
path.push(origin);
|
||||||
|
if let Some(id) = self.id::<C>(origin) {
|
||||||
|
return Some((id, path));
|
||||||
|
}
|
||||||
|
origin = self.parents.get(&origin).copied().flatten()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&mut self, active: &ActiveData) {
|
||||||
|
self.parents.insert(active.id, active.parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn undraw(&mut self, active: &ActiveData) {
|
||||||
|
self.parents.remove(&active.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take<C: Controller<Rsc>>(&mut self, id: ControllerId) -> Option<C> {
|
||||||
|
if id.kind != TypeId::of::<C>() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
!self.borrowed.contains(&id),
|
||||||
|
"a controller cannot re-enter itself while it is handling input"
|
||||||
|
);
|
||||||
|
let boxed = self.by_widget.get_mut(&id.host)?.remove(&id.kind)?;
|
||||||
|
self.borrowed.insert(id);
|
||||||
|
let boxed = boxed.into_any();
|
||||||
|
boxed.downcast().ok().map(|boxed| *boxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn put<C: Controller<Rsc>>(&mut self, id: ControllerId, controller: C) {
|
||||||
|
debug_assert_eq!(id.kind, TypeId::of::<C>());
|
||||||
|
assert!(
|
||||||
|
self.borrowed.remove(&id),
|
||||||
|
"restored an unborrowed controller"
|
||||||
|
);
|
||||||
|
if self.finish_removed_host(id.host) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let old = self
|
||||||
|
.by_widget
|
||||||
|
.entry(id.host)
|
||||||
|
.or_default()
|
||||||
|
.insert(id.kind, Box::new(controller));
|
||||||
|
debug_assert!(old.is_none(), "a controller was re-entered while borrowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_dyn(&mut self, id: ControllerId) -> Option<Box<dyn Controller<Rsc>>> {
|
||||||
|
assert!(
|
||||||
|
!self.borrowed.contains(&id),
|
||||||
|
"a controller cannot re-enter itself while it is handling input"
|
||||||
|
);
|
||||||
|
let controller = self.by_widget.get_mut(&id.host)?.remove(&id.kind)?;
|
||||||
|
self.borrowed.insert(id);
|
||||||
|
Some(controller)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn put_dyn(&mut self, id: ControllerId, controller: Box<dyn Controller<Rsc>>) {
|
||||||
|
assert!(
|
||||||
|
self.borrowed.remove(&id),
|
||||||
|
"restored an unborrowed controller"
|
||||||
|
);
|
||||||
|
if self.finish_removed_host(id.host) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let old = self
|
||||||
|
.by_widget
|
||||||
|
.entry(id.host)
|
||||||
|
.or_default()
|
||||||
|
.insert(id.kind, controller);
|
||||||
|
debug_assert!(old.is_none(), "a controller was re-entered while borrowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_command_target(&mut self, target: Option<ControllerId>) {
|
||||||
|
self.command_target = target;
|
||||||
|
self.command_target_revision = self.command_target_revision.wrapping_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command_target(&self) -> Option<ControllerId> {
|
||||||
|
self.command_target
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn command_target_revision(&self) -> u64 {
|
||||||
|
self.command_target_revision
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn command_boundary(&self) -> Option<WidgetId> {
|
||||||
|
self.command_boundary
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_command_boundary(&mut self, boundary: Option<WidgetId>) {
|
||||||
|
self.command_boundary = boundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, host: WidgetId) {
|
||||||
|
self.by_widget.remove(&host);
|
||||||
|
self.parents.remove(&host);
|
||||||
|
if self.borrowed.iter().any(|id| id.host == host) {
|
||||||
|
self.removed_while_borrowed.insert(host);
|
||||||
|
}
|
||||||
|
if self.command_target.is_some_and(|id| id.host == host) {
|
||||||
|
self.command_target = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn take_command_target(
|
||||||
|
&mut self,
|
||||||
|
) -> Option<(ControllerId, Box<dyn Controller<Rsc>>)> {
|
||||||
|
let id = self.command_target?;
|
||||||
|
match self.take_dyn(id) {
|
||||||
|
Some(controller) => Some((id, controller)),
|
||||||
|
None => {
|
||||||
|
self.command_target = None;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn restore(&mut self, id: ControllerId, controller: Box<dyn Controller<Rsc>>) {
|
||||||
|
self.put_dyn(id, controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true when a host disappeared during its controller callback,
|
||||||
|
/// in which case restoring the temporarily extracted value would revive
|
||||||
|
/// state belonging to a dead widget generation.
|
||||||
|
fn finish_removed_host(&mut self, host: WidgetId) -> bool {
|
||||||
|
if !self.removed_while_borrowed.contains(&host) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if !self.borrowed.iter().any(|id| id.host == host) {
|
||||||
|
self.removed_while_borrowed.remove(&host);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ActiveData, Event, EventCtx, EventFn, EventIdCtx, EventLike, HasEvents, IdLike, LayerId,
|
ActiveData, ControllerManager, Event, EventCtx, EventFn, EventIdCtx, EventLike, HasEvents,
|
||||||
WeakWidget, WidgetEventFn, WidgetId,
|
IdLike, LayerId, WeakWidget, WidgetEventFn, WidgetId,
|
||||||
util::{HashMap, HashSet, TypeMap},
|
util::{HashMap, HashSet, TypeMap},
|
||||||
};
|
};
|
||||||
use std::{any::TypeId, rc::Rc};
|
use std::{any::TypeId, rc::Rc};
|
||||||
@@ -8,13 +8,15 @@ use std::{any::TypeId, rc::Rc};
|
|||||||
pub struct EventManager<Rsc> {
|
pub struct EventManager<Rsc> {
|
||||||
widget_to_types: HashMap<WidgetId, HashSet<TypeId>>,
|
widget_to_types: HashMap<WidgetId, HashSet<TypeId>>,
|
||||||
types: TypeMap<dyn EventManagerLike<Rsc>>,
|
types: TypeMap<dyn EventManagerLike<Rsc>>,
|
||||||
|
pub controllers: ControllerManager<Rsc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Rsc> Default for EventManager<Rsc> {
|
impl<Rsc: 'static> Default for EventManager<Rsc> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
widget_to_types: Default::default(),
|
widget_to_types: Default::default(),
|
||||||
types: Default::default(),
|
types: Default::default(),
|
||||||
|
controllers: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,15 +56,18 @@ impl<Rsc: HasEvents + 'static> EventsLike for EventManager<Rsc> {
|
|||||||
for t in self.widget_to_types.get(&id).into_flat_iter() {
|
for t in self.widget_to_types.get(&id).into_flat_iter() {
|
||||||
self.types.get_mut(t).unwrap().remove(id);
|
self.types.get_mut(t).unwrap().remove(id);
|
||||||
}
|
}
|
||||||
|
self.controllers.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&mut self, active: &ActiveData) {
|
fn draw(&mut self, active: &ActiveData) {
|
||||||
|
self.controllers.draw(active);
|
||||||
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
|
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
|
||||||
self.types.get_mut(t).unwrap().draw(active);
|
self.types.get_mut(t).unwrap().draw(active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn undraw(&mut self, active: &ActiveData) {
|
fn undraw(&mut self, active: &ActiveData) {
|
||||||
|
self.controllers.undraw(active);
|
||||||
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
|
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
|
||||||
self.types.get_mut(t).unwrap().undraw(active);
|
self.types.get_mut(t).unwrap().undraw(active);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
mod controller;
|
||||||
mod ctx;
|
mod ctx;
|
||||||
mod manager;
|
mod manager;
|
||||||
mod rsc;
|
mod rsc;
|
||||||
|
|
||||||
|
pub use controller::*;
|
||||||
pub use ctx::*;
|
pub use ctx::*;
|
||||||
pub use manager::*;
|
pub use manager::*;
|
||||||
pub use rsc::*;
|
pub use rsc::*;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Event, EventCtx, EventLike, EventManager, IdLike, UiRsc, WeakWidget, Widget, WidgetEventFn,
|
Command, CommandResult, Controller, ControllerId, Event, EventCtx, EventLike, EventManager,
|
||||||
|
IdLike, UiRsc, WeakWidget, Widget, WidgetEventFn,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait HasState: 'static {
|
pub trait HasState: 'static {
|
||||||
@@ -18,6 +19,75 @@ pub trait HasEvents: Sized + UiRsc + HasState {
|
|||||||
) {
|
) {
|
||||||
self.events_mut().register(id, event, f);
|
self.events_mut().register(id, event, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn register_controller<W: ?Sized, C: Controller<Self>>(
|
||||||
|
&mut self,
|
||||||
|
id: WeakWidget<W>,
|
||||||
|
controller: C,
|
||||||
|
) {
|
||||||
|
self.events_mut().controllers.register(id.id(), controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_controller<C: Controller<Self>, T>(
|
||||||
|
&mut self,
|
||||||
|
id: ControllerId,
|
||||||
|
f: impl FnOnce(&mut C, &mut Self) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
|
let mut controller = self.events_mut().controllers.take::<C>(id)?;
|
||||||
|
let result = f(&mut controller, self);
|
||||||
|
self.events_mut().controllers.put(id, controller);
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_nearest_controller<C: Controller<Self>, T>(
|
||||||
|
&mut self,
|
||||||
|
origin: impl IdLike,
|
||||||
|
f: impl FnOnce(ControllerId, &mut C, &mut Self) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
|
let id = self.events().controllers.nearest_id::<C>(origin.id())?;
|
||||||
|
self.with_controller(id, |controller, rsc| f(id, controller, rsc))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_command_target(&mut self, target: Option<ControllerId>) {
|
||||||
|
self.events_mut().controllers.set_command_target(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_command(&mut self, command: Command) -> CommandResult {
|
||||||
|
let revision = self.events().controllers.command_target_revision();
|
||||||
|
if self
|
||||||
|
.events()
|
||||||
|
.controllers
|
||||||
|
.command_target()
|
||||||
|
.is_some_and(|target| {
|
||||||
|
self.events().controllers.command_boundary() == Some(target.host())
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return CommandResult::Unused;
|
||||||
|
}
|
||||||
|
let Some((id, mut controller)) = self.events_mut().controllers.take_command_target() else {
|
||||||
|
return CommandResult::Unused;
|
||||||
|
};
|
||||||
|
let result = controller.command(command, self);
|
||||||
|
self.events_mut().controllers.restore(id, controller);
|
||||||
|
if command == Command::Escape
|
||||||
|
&& result != CommandResult::Unused
|
||||||
|
&& self.events().controllers.command_target_revision() == revision
|
||||||
|
{
|
||||||
|
self.set_command_target(None);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn run_command_before(&mut self, command: Command, boundary: impl IdLike) -> CommandResult {
|
||||||
|
let old = self.events().controllers.command_boundary();
|
||||||
|
self.events_mut()
|
||||||
|
.controllers
|
||||||
|
.set_command_boundary(Some(boundary.id()));
|
||||||
|
let result = self.run_command(command);
|
||||||
|
self.events_mut().controllers.set_command_boundary(old);
|
||||||
|
result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait RunEvents: HasEvents {
|
pub trait RunEvents: HasEvents {
|
||||||
|
|||||||
@@ -30,5 +30,3 @@ pub use primitive::*;
|
|||||||
pub use render::*;
|
pub use render::*;
|
||||||
pub use ui::*;
|
pub use ui::*;
|
||||||
pub use widget::*;
|
pub use widget::*;
|
||||||
|
|
||||||
pub type UiColor = primitive::Color<u8>;
|
|
||||||
+479
-132
@@ -1,171 +1,518 @@
|
|||||||
use std::marker::Destruct;
|
use crate::util::{Dirty, RefCounter};
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
fmt,
|
||||||
|
rc::Rc,
|
||||||
|
sync::mpsc::{Receiver, Sender, channel},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Encoded, straight-alpha sRGB at an input boundary.
|
||||||
|
///
|
||||||
|
/// Palette literals, decoded images and colour glyph bitmaps use this
|
||||||
|
/// convention. A solid paint is converted to linear light when it enters the
|
||||||
|
/// paint table; the renderer never performs colour arithmetic on these bytes.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Clone, Copy, Hash, PartialEq, Eq, bytemuck::Zeroable, Debug)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct Color<T> {
|
pub struct Srgba8 {
|
||||||
pub r: T,
|
pub r: u8,
|
||||||
pub g: T,
|
pub g: u8,
|
||||||
pub b: T,
|
pub b: u8,
|
||||||
pub a: T,
|
pub a: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Required by parley's `Brush`, which every text style is generic over. Opaque
|
impl Srgba8 {
|
||||||
/// black rather than transparent: a brush that was never set should be visible
|
pub const BLACK: Self = Self::rgb(0, 0, 0);
|
||||||
/// and obviously unstyled, not invisible.
|
pub const WHITE: Self = Self::rgb(255, 255, 255);
|
||||||
impl<T: ColorNum> Default for Color<T> {
|
pub const GRAY: Self = Self::rgb(127, 127, 127);
|
||||||
|
pub const RED: Self = Self::rgb(255, 0, 0);
|
||||||
|
pub const ORANGE: Self = Self::rgb(255, 127, 0);
|
||||||
|
pub const YELLOW: Self = Self::rgb(255, 255, 0);
|
||||||
|
pub const LIME: Self = Self::rgb(127, 255, 0);
|
||||||
|
pub const GREEN: Self = Self::rgb(0, 255, 0);
|
||||||
|
pub const TURQUOISE: Self = Self::rgb(0, 255, 127);
|
||||||
|
pub const CYAN: Self = Self::rgb(0, 255, 255);
|
||||||
|
pub const SKY: Self = Self::rgb(0, 127, 255);
|
||||||
|
pub const BLUE: Self = Self::rgb(0, 0, 255);
|
||||||
|
pub const PURPLE: Self = Self::rgb(127, 0, 255);
|
||||||
|
pub const MAGENTA: Self = Self::rgb(255, 0, 255);
|
||||||
|
pub const NONE: Self = Self::new(0, 0, 0, 0);
|
||||||
|
|
||||||
|
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||||
|
Self { r, g, b, a }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||||
|
Self::new(r, g, b, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_linear(self) -> LinearRgba {
|
||||||
|
LinearRgba::new(
|
||||||
|
srgb_to_linear(self.r as f32 / 255.0),
|
||||||
|
srgb_to_linear(self.g as f32 / 255.0),
|
||||||
|
srgb_to_linear(self.b as f32 / 255.0),
|
||||||
|
self.a as f32 / 255.0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Straight-alpha RGBA in linear-light sRGB primaries.
|
||||||
|
///
|
||||||
|
/// This is Iris's working representation: manipulate and interpolate colours
|
||||||
|
/// here, then put the result in [`Paints`]. The GPU paint buffer stores this
|
||||||
|
/// exact layout as `vec4<f32>`.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
pub struct LinearRgba {
|
||||||
|
pub r: f32,
|
||||||
|
pub g: f32,
|
||||||
|
pub b: f32,
|
||||||
|
pub a: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinearRgba {
|
||||||
|
pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
|
||||||
|
pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
|
||||||
|
pub const NONE: Self = Self::new(0.0, 0.0, 0.0, 0.0);
|
||||||
|
|
||||||
|
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||||
|
Self { r, g, b, a }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
|
||||||
|
Self::new(r, g, b, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mul_rgb(self, amount: f32) -> Self {
|
||||||
|
Self::new(self.r * amount, self.g * amount, self.b * amount, self.a)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn darker(self, amount: f32) -> Self {
|
||||||
|
self.mul_rgb(1.0 - amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn brighter(self, amount: f32) -> Self {
|
||||||
|
Self::new(
|
||||||
|
self.r + (1.0 - self.r) * amount,
|
||||||
|
self.g + (1.0 - self.g) * amount,
|
||||||
|
self.b + (1.0 - self.b) * amount,
|
||||||
|
self.a,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_wgpu(self) -> wgpu::Color {
|
||||||
|
wgpu::Color {
|
||||||
|
r: self.r as f64,
|
||||||
|
g: self.g as f64,
|
||||||
|
b: self.b as f64,
|
||||||
|
a: self.a as f64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn srgb_to_linear(value: f32) -> f32 {
|
||||||
|
if value <= 0.04045 {
|
||||||
|
value / 12.92
|
||||||
|
} else {
|
||||||
|
((value + 0.055) / 1.055).powf(2.4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A description that can be registered in Iris's paint table.
|
||||||
|
///
|
||||||
|
/// Only solid paints exist today. Keeping registration behind this trait and
|
||||||
|
/// making primitives carry [`PaintId`] leaves one place to add gradient or
|
||||||
|
/// texture paint records later.
|
||||||
|
pub trait Paint: private::Sealed + 'static {
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn add_to(&self, paints: &mut Paints) -> PaintId;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn replace(&self, paints: &mut Paints, slot: u32);
|
||||||
|
|
||||||
|
/// Erases this definition so a widget can register it lazily on its
|
||||||
|
/// first draw. [`PaintId`] overrides this to stay a direct handle.
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn into_value(self) -> PaintValue
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
PaintValue::pending(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paint for Srgba8 {
|
||||||
|
fn add_to(&self, paints: &mut Paints) -> PaintId {
|
||||||
|
paints.add_linear(self.to_linear())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace(&self, paints: &mut Paints, slot: u32) {
|
||||||
|
paints.replace_linear(slot, self.to_linear());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paint for LinearRgba {
|
||||||
|
fn add_to(&self, paints: &mut Paints) -> PaintId {
|
||||||
|
paints.add_linear(*self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace(&self, paints: &mut Paints, slot: u32) {
|
||||||
|
paints.replace_linear(slot, *self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod private {
|
||||||
|
pub trait Sealed {}
|
||||||
|
impl Sealed for super::Srgba8 {}
|
||||||
|
impl Sealed for super::LinearRgba {}
|
||||||
|
impl Sealed for super::PaintId {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct PaintLease {
|
||||||
|
slot: u32,
|
||||||
|
counter: RefCounter,
|
||||||
|
send: Sender<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for PaintLease {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
slot: self.slot,
|
||||||
|
counter: self.counter.clone(),
|
||||||
|
send: self.send.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for PaintLease {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.counter.drop() {
|
||||||
|
let _ = self.send.send(self.slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A stable reference to one entry in a UI's paint table.
|
||||||
|
///
|
||||||
|
/// Built-in IDs name the same reserved entries in every [`Paints`]. IDs
|
||||||
|
/// returned by [`Paints::add`] retain their slot until the last clone held by a
|
||||||
|
/// widget, shaped text or retained draw is dropped.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PaintId {
|
||||||
|
slot: u32,
|
||||||
|
lease: Option<PaintLease>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaintId {
|
||||||
|
pub const BLACK: Self = Self::builtin(0);
|
||||||
|
pub const WHITE: Self = Self::builtin(1);
|
||||||
|
pub const GRAY: Self = Self::builtin(2);
|
||||||
|
pub const RED: Self = Self::builtin(3);
|
||||||
|
pub const ORANGE: Self = Self::builtin(4);
|
||||||
|
pub const YELLOW: Self = Self::builtin(5);
|
||||||
|
pub const LIME: Self = Self::builtin(6);
|
||||||
|
pub const GREEN: Self = Self::builtin(7);
|
||||||
|
pub const TURQUOISE: Self = Self::builtin(8);
|
||||||
|
pub const CYAN: Self = Self::builtin(9);
|
||||||
|
pub const SKY: Self = Self::builtin(10);
|
||||||
|
pub const BLUE: Self = Self::builtin(11);
|
||||||
|
pub const PURPLE: Self = Self::builtin(12);
|
||||||
|
pub const MAGENTA: Self = Self::builtin(13);
|
||||||
|
pub const NONE: Self = Self::builtin(14);
|
||||||
|
|
||||||
|
const fn builtin(slot: u32) -> Self {
|
||||||
|
Self { slot, lease: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn slot(&self) -> u32 {
|
||||||
|
self.slot
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_managed(&self) -> bool {
|
||||||
|
self.lease.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PaintId {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::BLACK
|
Self::BLACK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: ColorNum> Color<T> {
|
impl PartialEq for PaintId {
|
||||||
pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN);
|
fn eq(&self, other: &Self) -> bool {
|
||||||
pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX);
|
self.slot == other.slot
|
||||||
pub const GRAY: Self = Self::rgb(T::MID, T::MID, T::MID);
|
|
||||||
|
|
||||||
pub const RED: Self = Self::rgb(T::MAX, T::MIN, T::MIN);
|
|
||||||
pub const ORANGE: Self = Self::rgb(T::MAX, T::MID, T::MIN);
|
|
||||||
pub const YELLOW: Self = Self::rgb(T::MAX, T::MAX, T::MIN);
|
|
||||||
pub const LIME: Self = Self::rgb(T::MID, T::MAX, T::MIN);
|
|
||||||
pub const GREEN: Self = Self::rgb(T::MIN, T::MAX, T::MIN);
|
|
||||||
pub const TURQUOISE: Self = Self::rgb(T::MIN, T::MAX, T::MID);
|
|
||||||
pub const CYAN: Self = Self::rgb(T::MIN, T::MAX, T::MAX);
|
|
||||||
pub const SKY: Self = Self::rgb(T::MIN, T::MID, T::MAX);
|
|
||||||
pub const BLUE: Self = Self::rgb(T::MIN, T::MIN, T::MAX);
|
|
||||||
pub const PURPLE: Self = Self::rgb(T::MID, T::MIN, T::MAX);
|
|
||||||
pub const MAGENTA: Self = Self::rgb(T::MAX, T::MIN, T::MAX);
|
|
||||||
|
|
||||||
pub const NONE: Self = Self::new(T::MIN, T::MIN, T::MIN, T::MIN);
|
|
||||||
|
|
||||||
pub const fn new(r: T, g: T, b: T, a: T) -> Self {
|
|
||||||
Self { r, g, b, a }
|
|
||||||
}
|
|
||||||
pub const fn rgb(r: T, g: T, b: T) -> Self {
|
|
||||||
Self { r, g, b, a: T::MAX }
|
|
||||||
}
|
|
||||||
pub fn alpha(mut self, a: T) -> Self {
|
|
||||||
self.a = a;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_arr(self) -> [T; 4] {
|
|
||||||
[self.r, self.g, self.b, self.a]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const trait F32Conversion {
|
impl Eq for PaintId {}
|
||||||
fn to(self) -> f32;
|
|
||||||
fn from(x: f32) -> Self;
|
impl Paint for PaintId {
|
||||||
|
fn add_to(&self, _paints: &mut Paints) -> PaintId {
|
||||||
|
self.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ColorNum {
|
fn replace(&self, paints: &mut Paints, slot: u32) {
|
||||||
const MIN: Self;
|
let value = paints.entries[self.slot as usize];
|
||||||
const MID: Self;
|
paints.replace_linear(slot, value);
|
||||||
const MAX: Self;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! map_rgb {
|
fn into_value(self) -> PaintValue {
|
||||||
($x:ident,$self:ident, $e:tt) => {
|
PaintValue(PaintValueInner::Id(self))
|
||||||
#[allow(unused_braces)]
|
}
|
||||||
Self {
|
}
|
||||||
r: {
|
|
||||||
let $x = $self.r;
|
struct PendingPaint {
|
||||||
$e
|
definition: Box<dyn Paint>,
|
||||||
},
|
resolved: RefCell<Option<PaintId>>,
|
||||||
g: {
|
}
|
||||||
let $x = $self.g;
|
|
||||||
$e
|
#[derive(Clone)]
|
||||||
},
|
enum PaintValueInner {
|
||||||
b: {
|
Id(PaintId),
|
||||||
let $x = $self.b;
|
Pending(Rc<PendingPaint>),
|
||||||
$e
|
}
|
||||||
},
|
|
||||||
a: $self.a,
|
/// A widget property containing either an existing paint-table ID or a paint
|
||||||
|
/// definition that will receive an ID the first time it is drawn.
|
||||||
|
///
|
||||||
|
/// Pending definitions are shared across clones and registered only once.
|
||||||
|
/// Each property replaces its own pending variant with the resulting direct
|
||||||
|
/// ID after that first resolution, so later draws take the direct path.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PaintValue(PaintValueInner);
|
||||||
|
|
||||||
|
impl PaintValue {
|
||||||
|
fn pending(paint: impl Paint) -> Self {
|
||||||
|
Self(PaintValueInner::Pending(Rc::new(PendingPaint {
|
||||||
|
definition: Box::new(paint),
|
||||||
|
resolved: RefCell::new(None),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve(&mut self, paints: &mut Paints) -> &PaintId {
|
||||||
|
if let PaintValueInner::Pending(pending) = &self.0 {
|
||||||
|
let resolved = pending.resolved.borrow().clone();
|
||||||
|
let id = match resolved {
|
||||||
|
Some(id) => id,
|
||||||
|
None => {
|
||||||
|
let id = pending.definition.add_to(paints);
|
||||||
|
*pending.resolved.borrow_mut() = Some(id.clone());
|
||||||
|
id
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
self.0 = PaintValueInner::Id(id);
|
||||||
|
}
|
||||||
|
let PaintValueInner::Id(id) = &self.0 else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: ColorNum + const F32Conversion> Color<T>
|
pub fn is(&self, id: &PaintId) -> bool {
|
||||||
where
|
match &self.0 {
|
||||||
Self: const Destruct,
|
PaintValueInner::Id(current) => current == id,
|
||||||
{
|
PaintValueInner::Pending(pending) => pending.resolved.borrow().as_ref() == Some(id),
|
||||||
pub const fn mul_rgb(self, amt: impl const F32Conversion) -> Self {
|
}
|
||||||
let amt = amt.to();
|
}
|
||||||
map_rgb!(x, self, { T::from(x.to() * amt) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn add_rgb(self, amt: impl const F32Conversion) -> Self {
|
impl fmt::Debug for PaintValue {
|
||||||
let amt = amt.to();
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
map_rgb!(x, self, { T::from(x.to() + amt) })
|
match &self.0 {
|
||||||
|
PaintValueInner::Id(id) => f.debug_tuple("PaintValue").field(id).finish(),
|
||||||
|
PaintValueInner::Pending(_) => f.write_str("PaintValue(Pending)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn darker(self, amt: f32) -> Self {
|
const BUILTIN_PAINTS: [Srgba8; 15] = [
|
||||||
self.mul_rgb(1.0 - amt)
|
Srgba8::BLACK,
|
||||||
|
Srgba8::WHITE,
|
||||||
|
Srgba8::GRAY,
|
||||||
|
Srgba8::RED,
|
||||||
|
Srgba8::ORANGE,
|
||||||
|
Srgba8::YELLOW,
|
||||||
|
Srgba8::LIME,
|
||||||
|
Srgba8::GREEN,
|
||||||
|
Srgba8::TURQUOISE,
|
||||||
|
Srgba8::CYAN,
|
||||||
|
Srgba8::SKY,
|
||||||
|
Srgba8::BLUE,
|
||||||
|
Srgba8::PURPLE,
|
||||||
|
Srgba8::MAGENTA,
|
||||||
|
Srgba8::NONE,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// CPU-side paint table and the dirty set for its GPU mirror.
|
||||||
|
pub struct Paints {
|
||||||
|
entries: Vec<LinearRgba>,
|
||||||
|
free: Vec<u32>,
|
||||||
|
dirty: Dirty,
|
||||||
|
send: Sender<u32>,
|
||||||
|
recv: Receiver<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn brighter(self, amt: f32) -> Self {
|
impl Paints {
|
||||||
map_rgb!(x, self, {
|
pub fn new() -> Self {
|
||||||
let x = x.to();
|
let (send, recv) = channel();
|
||||||
T::from(x + (1.0 - x) * amt)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn map_rgb(self, f: impl Fn(T) -> T) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
r: f(self.r),
|
entries: BUILTIN_PAINTS.map(Srgba8::to_linear).to_vec(),
|
||||||
g: f(self.g),
|
free: Vec::new(),
|
||||||
b: f(self.b),
|
dirty: Dirty::new_all(),
|
||||||
a: self.a,
|
send,
|
||||||
|
recv,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn srgb(r: T, g: T, b: T) -> Self {
|
pub fn add(&mut self, paint: impl Paint) -> PaintId {
|
||||||
Self {
|
paint.add_to(self)
|
||||||
r: s_to_l(r),
|
|
||||||
g: s_to_l(g),
|
|
||||||
b: s_to_l(b),
|
|
||||||
a: T::MAX,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn s_to_l<T: F32Conversion>(x: T) -> T {
|
fn add_linear(&mut self, value: LinearRgba) -> PaintId {
|
||||||
let x = x.to();
|
self.free_released();
|
||||||
T::from(if x <= 0.0405 {
|
let slot = if let Some(slot) = self.free.pop() {
|
||||||
x / 12.92
|
self.entries[slot as usize] = value;
|
||||||
|
self.dirty.mark(slot as usize);
|
||||||
|
slot
|
||||||
} else {
|
} else {
|
||||||
((x + 0.055) / 1.055).powf(2.4)
|
let slot = self.entries.len() as u32;
|
||||||
})
|
self.entries.push(value);
|
||||||
}
|
self.dirty.mark(slot as usize);
|
||||||
|
slot
|
||||||
impl ColorNum for u8 {
|
};
|
||||||
const MIN: Self = u8::MIN;
|
PaintId {
|
||||||
const MID: Self = u8::MAX / 2;
|
slot,
|
||||||
const MAX: Self = u8::MAX;
|
lease: Some(PaintLease {
|
||||||
}
|
slot,
|
||||||
|
counter: RefCounter::new(),
|
||||||
impl ColorNum for f32 {
|
send: self.send.clone(),
|
||||||
const MIN: Self = 0.0;
|
}),
|
||||||
const MID: Self = 0.5;
|
|
||||||
const MAX: Self = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl bytemuck::Pod for Color<u8> {}
|
|
||||||
|
|
||||||
const impl F32Conversion for f32 {
|
|
||||||
fn to(self) -> f32 {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn from(x: f32) -> Self {
|
|
||||||
x
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const impl F32Conversion for u8 {
|
/// Replaces one managed paint in place. Every primitive keeps the same
|
||||||
fn to(self) -> f32 {
|
/// index, so a theme change dirties this table and no primitive buffer.
|
||||||
self as f32 / 255.0
|
pub fn set(&mut self, id: &PaintId, paint: impl Paint) {
|
||||||
|
assert!(
|
||||||
|
id.is_managed(),
|
||||||
|
"a reserved built-in paint cannot be replaced; allocate a theme slot with Paints::add"
|
||||||
|
);
|
||||||
|
paint.replace(self, id.slot);
|
||||||
}
|
}
|
||||||
fn from(x: f32) -> Self {
|
|
||||||
(x * 255.0).clamp(0.0, 255.0) as Self
|
fn replace_linear(&mut self, slot: u32, value: LinearRgba) {
|
||||||
|
self.entries[slot as usize] = value;
|
||||||
|
self.dirty.mark(slot as usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, id: &PaintId) -> LinearRgba {
|
||||||
|
self.entries[id.slot as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn free_released(&mut self) {
|
||||||
|
for slot in self.recv.try_iter() {
|
||||||
|
self.entries[slot as usize] = LinearRgba::NONE;
|
||||||
|
self.dirty.mark(slot as usize);
|
||||||
|
self.free.push(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A new GPU device has no copy of this table even when the CPU-side UI
|
||||||
|
/// and its paint IDs survived an Android surface recreation.
|
||||||
|
pub fn reupload(&mut self) {
|
||||||
|
self.dirty.mark_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_upload(&mut self) -> (&[LinearRgba], &mut Dirty) {
|
||||||
|
(&self.entries, &mut self.dirty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Paints {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn srgb_bytes_become_linear_without_transforming_alpha() {
|
||||||
|
let got = Srgba8::new(17, 127, 255, 64).to_linear();
|
||||||
|
assert!((got.r - 0.005605).abs() < 0.000001);
|
||||||
|
assert!((got.g - 0.212231).abs() < 0.000001);
|
||||||
|
assert_eq!(got.b, 1.0);
|
||||||
|
assert!((got.a - 64.0 / 255.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changing_a_paint_keeps_its_id_and_dirties_only_its_slot() {
|
||||||
|
let mut paints = Paints::new();
|
||||||
|
let id = paints.add(Srgba8::rgb(17, 17, 27));
|
||||||
|
let slot = id.slot();
|
||||||
|
let (_, dirty) = paints.for_upload();
|
||||||
|
dirty.clear();
|
||||||
|
|
||||||
|
paints.set(&id, Srgba8::rgb(205, 214, 244));
|
||||||
|
let (_, dirty) = paints.for_upload();
|
||||||
|
assert!(dirty.contains(slot as usize));
|
||||||
|
assert_eq!(id.slot(), slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_released_paint_slot_is_reused_only_after_the_last_clone() {
|
||||||
|
let mut paints = Paints::new();
|
||||||
|
let id = paints.add(Srgba8::RED);
|
||||||
|
let slot = id.slot();
|
||||||
|
let held = id.clone();
|
||||||
|
drop(id);
|
||||||
|
paints.free_released();
|
||||||
|
let other = paints.add(Srgba8::GREEN);
|
||||||
|
assert_ne!(other.slot(), slot);
|
||||||
|
|
||||||
|
drop(held);
|
||||||
|
paints.free_released();
|
||||||
|
let reused = paints.add(Srgba8::BLUE);
|
||||||
|
assert_eq!(reused.slot(), slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cloned_pending_paints_register_once_and_then_become_direct_ids() {
|
||||||
|
let mut paints = Paints::new();
|
||||||
|
let mut first = Srgba8::rgb(17, 17, 27).into_value();
|
||||||
|
let mut second = first.clone();
|
||||||
|
let before = paints.entries.len();
|
||||||
|
|
||||||
|
let first_id = first.resolve(&mut paints).clone();
|
||||||
|
let second_id = second.resolve(&mut paints).clone();
|
||||||
|
|
||||||
|
assert_eq!(first_id, second_id);
|
||||||
|
assert_eq!(paints.entries.len(), before + 1);
|
||||||
|
assert!(first.is(&first_id));
|
||||||
|
assert!(second.is(&first_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn independently_constructed_inline_solids_get_independent_slots() {
|
||||||
|
let mut paints = Paints::new();
|
||||||
|
let mut first = Srgba8::rgb(23, 42, 71).into_value();
|
||||||
|
let mut second = Srgba8::rgb(23, 42, 71).into_value();
|
||||||
|
let before = paints.entries.len();
|
||||||
|
|
||||||
|
let first_id = first.resolve(&mut paints).clone();
|
||||||
|
let second_id = second.resolve(&mut paints).clone();
|
||||||
|
assert_ne!(first_id, second_id);
|
||||||
|
assert_eq!(paints.entries.len(), before + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_theme_paints_with_equal_values_remain_independent() {
|
||||||
|
let mut paints = Paints::new();
|
||||||
|
let first = paints.add(Srgba8::rgb(23, 42, 71));
|
||||||
|
let second = paints.add(Srgba8::rgb(23, 42, 71));
|
||||||
|
assert_ne!(first.slot(), second.slot());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, util::Vec2};
|
use crate::{Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, util::Vec2};
|
||||||
use parley::{
|
use parley::{
|
||||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, FontStyle, FontWeight,
|
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, FontStyle, FontWeight,
|
||||||
GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||||
@@ -32,7 +32,7 @@ pub struct FontDiagnostics {
|
|||||||
|
|
||||||
pub struct TextData {
|
pub struct TextData {
|
||||||
pub font_cx: FontContext,
|
pub font_cx: FontContext,
|
||||||
pub layout_cx: LayoutContext<UiColor>,
|
pub layout_cx: LayoutContext<PaintId>,
|
||||||
scale_cx: ScaleContext,
|
scale_cx: ScaleContext,
|
||||||
pub atlas: GlyphAtlas,
|
pub atlas: GlyphAtlas,
|
||||||
/// Physical pixels per dp -- a second copy of
|
/// Physical pixels per dp -- a second copy of
|
||||||
@@ -240,8 +240,13 @@ impl TextData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> {
|
pub fn place(
|
||||||
|
&mut self,
|
||||||
|
buffer: &TextBuffer,
|
||||||
|
textures: &mut Textures,
|
||||||
|
) -> (Vec<PlacedGlyph>, Vec<PaintId>) {
|
||||||
let mut placed = Vec::new();
|
let mut placed = Vec::new();
|
||||||
|
let mut paints = Vec::new();
|
||||||
for line in buffer.layout.lines() {
|
for line in buffer.layout.lines() {
|
||||||
for item in line.items() {
|
for item in line.items() {
|
||||||
let PositionedLayoutItem::GlyphRun(run) = item else {
|
let PositionedLayoutItem::GlyphRun(run) = item else {
|
||||||
@@ -250,7 +255,10 @@ impl TextData {
|
|||||||
let font = run.run().font();
|
let font = run.run().font();
|
||||||
let font_size = run.run().font_size();
|
let font_size = run.run().font_size();
|
||||||
let coords = run.run().normalized_coords();
|
let coords = run.run().normalized_coords();
|
||||||
let run_color = run.style().brush;
|
let run_color = run.style().brush.clone();
|
||||||
|
if !paints.contains(&run_color) {
|
||||||
|
paints.push(run_color.clone());
|
||||||
|
}
|
||||||
let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize)
|
let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize)
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
@@ -301,12 +309,12 @@ impl TextData {
|
|||||||
glyph.x.floor() + entry.left as f32,
|
glyph.x.floor() + entry.left as f32,
|
||||||
glyph.y.floor() - entry.top as f32,
|
glyph.y.floor() - entry.top as f32,
|
||||||
),
|
),
|
||||||
color: run_color,
|
paint: run_color.slot(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
placed
|
(placed, paints)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(
|
pub fn render(
|
||||||
@@ -318,11 +326,12 @@ impl TextData {
|
|||||||
density: f32,
|
density: f32,
|
||||||
) -> RenderedText {
|
) -> RenderedText {
|
||||||
buffer.shape(self, attrs, width, density);
|
buffer.shape(self, attrs, width, density);
|
||||||
let glyphs = self.place(buffer, textures);
|
let (glyphs, paints) = self.place(buffer, textures);
|
||||||
RenderedText {
|
RenderedText {
|
||||||
glyphs: std::sync::Arc::new(glyphs),
|
glyphs: std::sync::Arc::new(glyphs),
|
||||||
|
paints: std::sync::Arc::new(paints),
|
||||||
size: buffer.size(),
|
size: buffer.size(),
|
||||||
color: attrs.color,
|
color: attrs.color.clone(),
|
||||||
generation: self.atlas.generation(),
|
generation: self.atlas.generation(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,7 +368,7 @@ impl Family {
|
|||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct SpanStyle {
|
pub struct SpanStyle {
|
||||||
pub range: Range<usize>,
|
pub range: Range<usize>,
|
||||||
pub color: Option<UiColor>,
|
pub color: Option<PaintId>,
|
||||||
pub family: Option<Family>,
|
pub family: Option<Family>,
|
||||||
pub font_size: Option<f32>,
|
pub font_size: Option<f32>,
|
||||||
pub bold: bool,
|
pub bold: bool,
|
||||||
@@ -379,7 +388,7 @@ impl SpanStyle {
|
|||||||
underline: false,
|
underline: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn color(mut self, color: UiColor) -> Self {
|
pub fn color(mut self, color: PaintId) -> Self {
|
||||||
self.color = Some(color);
|
self.color = Some(color);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -407,7 +416,7 @@ impl SpanStyle {
|
|||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct TextAttrs {
|
pub struct TextAttrs {
|
||||||
pub color: UiColor,
|
pub color: PaintId,
|
||||||
pub font_size: f32,
|
pub font_size: f32,
|
||||||
pub line_height: f32,
|
pub line_height: f32,
|
||||||
pub family: Family,
|
pub family: Family,
|
||||||
@@ -421,7 +430,7 @@ impl Default for TextAttrs {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let size = 16.0;
|
let size = 16.0;
|
||||||
Self {
|
Self {
|
||||||
color: UiColor::WHITE,
|
color: PaintId::WHITE,
|
||||||
font_size: size,
|
font_size: size,
|
||||||
line_height: size * LINE_HEIGHT_MULT,
|
line_height: size * LINE_HEIGHT_MULT,
|
||||||
family: Family::SansSerif,
|
family: Family::SansSerif,
|
||||||
@@ -436,7 +445,7 @@ impl Default for TextAttrs {
|
|||||||
/// them apart is how they get out of step.
|
/// them apart is how they get out of step.
|
||||||
pub struct TextBuffer {
|
pub struct TextBuffer {
|
||||||
text: String,
|
text: String,
|
||||||
layout: Layout<UiColor>,
|
layout: Layout<PaintId>,
|
||||||
spans: Vec<SpanStyle>,
|
spans: Vec<SpanStyle>,
|
||||||
shaped: Option<(TextAttrs, Option<f32>, f32)>,
|
shaped: Option<(TextAttrs, Option<f32>, f32)>,
|
||||||
}
|
}
|
||||||
@@ -464,7 +473,7 @@ impl TextBuffer {
|
|||||||
&self.text
|
&self.text
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layout(&self) -> &Layout<UiColor> {
|
pub fn layout(&self) -> &Layout<PaintId> {
|
||||||
&self.layout
|
&self.layout
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,11 +524,11 @@ impl TextBuffer {
|
|||||||
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
|
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
|
||||||
attrs.line_height * density,
|
attrs.line_height * density,
|
||||||
)));
|
)));
|
||||||
builder.push_default(StyleProperty::Brush(attrs.color));
|
builder.push_default(StyleProperty::Brush(attrs.color.clone()));
|
||||||
for (span, family) in self.spans.iter().zip(&span_families) {
|
for (span, family) in self.spans.iter().zip(&span_families) {
|
||||||
let range = span.range.clone();
|
let range = span.range.clone();
|
||||||
if let Some(color) = span.color {
|
if let Some(color) = &span.color {
|
||||||
builder.push(StyleProperty::Brush(color), range.clone());
|
builder.push(StyleProperty::Brush(color.clone()), range.clone());
|
||||||
}
|
}
|
||||||
if let Some(family) = family {
|
if let Some(family) = family {
|
||||||
builder.push(StyleProperty::FontFamily(family.family()), range.clone());
|
builder.push(StyleProperty::FontFamily(family.family()), range.clone());
|
||||||
@@ -563,8 +572,10 @@ fn hash_coords(coords: &[i16]) -> u64 {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RenderedText {
|
pub struct RenderedText {
|
||||||
pub glyphs: std::sync::Arc<Vec<PlacedGlyph>>,
|
pub glyphs: std::sync::Arc<Vec<PlacedGlyph>>,
|
||||||
|
/// The unique handles whose compact slots the glyphs above carry.
|
||||||
|
pub paints: std::sync::Arc<Vec<PaintId>>,
|
||||||
pub size: Vec2,
|
pub size: Vec2,
|
||||||
pub color: UiColor,
|
pub color: PaintId,
|
||||||
/// The [`GlyphAtlas::generation`] the glyphs above were placed against.
|
/// The [`GlyphAtlas::generation`] the glyphs above were placed against.
|
||||||
/// A holder must re-render rather than re-emit these quads once the
|
/// A holder must re-render rather than re-emit these quads once the
|
||||||
/// atlas has moved on (`GlyphAtlas::clear`'s doc says what happens
|
/// atlas has moved on (`GlyphAtlas::clear`'s doc says what happens
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
PatchRect, TextureHandle, Textures, UiColor,
|
PatchRect, TextureHandle, Textures,
|
||||||
util::{HashMap, Vec2},
|
util::{HashMap, Vec2},
|
||||||
};
|
};
|
||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
@@ -202,5 +202,5 @@ fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
|
|||||||
pub struct PlacedGlyph {
|
pub struct PlacedGlyph {
|
||||||
pub entry: GlyphEntry,
|
pub entry: GlyphEntry,
|
||||||
pub offset: Vec2,
|
pub offset: Vec2,
|
||||||
pub color: UiColor,
|
pub paint: u32,
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,46 @@ pub use sdf::{distance_from_rect, rounded_rect_coverage};
|
|||||||
|
|
||||||
pub const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
pub const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
||||||
|
|
||||||
|
/// The advertised swapchain format and the sRGB view Iris renders through.
|
||||||
|
/// A backend may advertise only the non-sRGB member of an RGBA/BGRA pair;
|
||||||
|
/// wgpu permits its sRGB counterpart as a configured view format.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SurfaceFormat {
|
||||||
|
pub surface: TextureFormat,
|
||||||
|
pub view: TextureFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn srgb_surface_format(caps: &SurfaceCapabilities) -> Result<SurfaceFormat, String> {
|
||||||
|
let supports_srgb_space = |format| caps.color_spaces(format).contains(SurfaceColorSpaces::SRGB);
|
||||||
|
if let Some(surface) = caps
|
||||||
|
.formats
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|format| format.is_srgb() && supports_srgb_space(*format))
|
||||||
|
{
|
||||||
|
return Ok(SurfaceFormat {
|
||||||
|
surface,
|
||||||
|
view: surface,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(surface) = caps
|
||||||
|
.formats
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|format| format.add_srgb_suffix().is_srgb() && supports_srgb_space(*format))
|
||||||
|
{
|
||||||
|
return Ok(SurfaceFormat {
|
||||||
|
surface,
|
||||||
|
view: surface.add_srgb_suffix(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(format!(
|
||||||
|
"the surface has no RGBA/BGRA format with an sRGB render view and sRGB output colour \
|
||||||
|
space; advertised default formats: {:?}",
|
||||||
|
caps.formats
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn device_limits() -> Limits {
|
pub fn device_limits() -> Limits {
|
||||||
Limits {
|
Limits {
|
||||||
max_buffer_size: 1 << 30,
|
max_buffer_size: 1 << 30,
|
||||||
@@ -91,6 +131,7 @@ pub struct UiRenderNode {
|
|||||||
instances: ArrBuf<PrimitiveInstance>,
|
instances: ArrBuf<PrimitiveInstance>,
|
||||||
masks: ArrBuf<Mask>,
|
masks: ArrBuf<Mask>,
|
||||||
move_offsets: ArrBuf<MoveOffset>,
|
move_offsets: ArrBuf<MoveOffset>,
|
||||||
|
paints: ArrBuf<crate::LinearRgba>,
|
||||||
masks_layout: BindGroupLayout,
|
masks_layout: BindGroupLayout,
|
||||||
masks_group: BindGroup,
|
masks_group: BindGroup,
|
||||||
}
|
}
|
||||||
@@ -196,13 +237,16 @@ impl UiRenderNode {
|
|||||||
let masks_resized = self.masks.update(device, queue, entries, dirty);
|
let masks_resized = self.masks.update(device, queue, entries, dirty);
|
||||||
let (entries, dirty) = ui.move_offsets.for_upload();
|
let (entries, dirty) = ui.move_offsets.for_upload();
|
||||||
let moves_resized = self.move_offsets.update(device, queue, entries, dirty);
|
let moves_resized = self.move_offsets.update(device, queue, entries, dirty);
|
||||||
if masks_resized || moves_resized || instances_resized {
|
let (entries, dirty) = ui.paints.for_upload();
|
||||||
|
let paints_resized = self.paints.update(device, queue, entries, dirty);
|
||||||
|
if masks_resized || moves_resized || instances_resized || paints_resized {
|
||||||
self.masks_group = Self::masks_group(
|
self.masks_group = Self::masks_group(
|
||||||
device,
|
device,
|
||||||
&self.masks_layout,
|
&self.masks_layout,
|
||||||
&self.masks,
|
&self.masks,
|
||||||
&self.move_offsets,
|
&self.move_offsets,
|
||||||
&self.instances,
|
&self.instances,
|
||||||
|
&self.paints,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let rebuild_main = self.textures.update(&mut ui.textures, &self.rsc_layout);
|
let rebuild_main = self.textures.update(&mut ui.textures, &self.rsc_layout);
|
||||||
@@ -212,6 +256,7 @@ impl UiRenderNode {
|
|||||||
FrameUpdateStats {
|
FrameUpdateStats {
|
||||||
masks_resized,
|
masks_resized,
|
||||||
moves_resized,
|
moves_resized,
|
||||||
|
paints_resized,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +276,7 @@ impl UiRenderNode {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
device: &Device,
|
device: &Device,
|
||||||
queue: &Queue,
|
queue: &Queue,
|
||||||
config: &SurfaceConfiguration,
|
target_format: TextureFormat,
|
||||||
window_size: impl Into<Vec2>,
|
window_size: impl Into<Vec2>,
|
||||||
) -> Result<Self, String> {
|
) -> Result<Self, String> {
|
||||||
let oom_scope = device.push_error_scope(ErrorFilter::OutOfMemory);
|
let oom_scope = device.push_error_scope(ErrorFilter::OutOfMemory);
|
||||||
@@ -305,12 +350,23 @@ impl UiRenderNode {
|
|||||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||||
"ui move offsets",
|
"ui move offsets",
|
||||||
);
|
);
|
||||||
|
let paints = ArrBuf::new(
|
||||||
|
device,
|
||||||
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||||
|
"ui paints",
|
||||||
|
);
|
||||||
|
|
||||||
let rsc_layout = Self::rsc_layout(device);
|
let rsc_layout = Self::rsc_layout(device);
|
||||||
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager);
|
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager);
|
||||||
let masks_layout = Self::masks_layout(device);
|
let masks_layout = Self::masks_layout(device);
|
||||||
let masks_group =
|
let masks_group = Self::masks_group(
|
||||||
Self::masks_group(device, &masks_layout, &masks, &move_offsets, &instances);
|
device,
|
||||||
|
&masks_layout,
|
||||||
|
&masks,
|
||||||
|
&move_offsets,
|
||||||
|
&instances,
|
||||||
|
&paints,
|
||||||
|
);
|
||||||
|
|
||||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||||
label: Some("UI Shape Pipeline Layout"),
|
label: Some("UI Shape Pipeline Layout"),
|
||||||
@@ -335,7 +391,7 @@ impl UiRenderNode {
|
|||||||
module: &shader,
|
module: &shader,
|
||||||
entry_point: Some("fs_main"),
|
entry_point: Some("fs_main"),
|
||||||
targets: &[Some(ColorTargetState {
|
targets: &[Some(ColorTargetState {
|
||||||
format: config.format,
|
format: target_format,
|
||||||
blend: Some(BlendState::ALPHA_BLENDING),
|
blend: Some(BlendState::ALPHA_BLENDING),
|
||||||
write_mask: ColorWrites::ALL,
|
write_mask: ColorWrites::ALL,
|
||||||
})],
|
})],
|
||||||
@@ -386,6 +442,7 @@ impl UiRenderNode {
|
|||||||
instances,
|
instances,
|
||||||
masks,
|
masks,
|
||||||
move_offsets,
|
move_offsets,
|
||||||
|
paints,
|
||||||
masks_layout,
|
masks_layout,
|
||||||
masks_group,
|
masks_group,
|
||||||
})
|
})
|
||||||
@@ -522,6 +579,16 @@ impl UiRenderNode {
|
|||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
},
|
},
|
||||||
|
BindGroupLayoutEntry {
|
||||||
|
binding: 3,
|
||||||
|
visibility: ShaderStages::FRAGMENT,
|
||||||
|
ty: BindingType::Buffer {
|
||||||
|
ty: BufferBindingType::Storage { read_only: true },
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
label: Some("ui masks"),
|
label: Some("ui masks"),
|
||||||
})
|
})
|
||||||
@@ -533,6 +600,7 @@ impl UiRenderNode {
|
|||||||
masks: &ArrBuf<Mask>,
|
masks: &ArrBuf<Mask>,
|
||||||
move_offsets: &ArrBuf<MoveOffset>,
|
move_offsets: &ArrBuf<MoveOffset>,
|
||||||
instances: &ArrBuf<PrimitiveInstance>,
|
instances: &ArrBuf<PrimitiveInstance>,
|
||||||
|
paints: &ArrBuf<crate::LinearRgba>,
|
||||||
) -> BindGroup {
|
) -> BindGroup {
|
||||||
device.create_bind_group(&BindGroupDescriptor {
|
device.create_bind_group(&BindGroupDescriptor {
|
||||||
layout,
|
layout,
|
||||||
@@ -549,6 +617,10 @@ impl UiRenderNode {
|
|||||||
binding: 2,
|
binding: 2,
|
||||||
resource: instances.buffer.as_entire_binding(),
|
resource: instances.buffer.as_entire_binding(),
|
||||||
},
|
},
|
||||||
|
BindGroupEntry {
|
||||||
|
binding: 3,
|
||||||
|
resource: paints.buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
label: Some("ui masks"),
|
label: Some("ui masks"),
|
||||||
})
|
})
|
||||||
@@ -576,4 +648,5 @@ impl UiRenderNode {
|
|||||||
pub struct FrameUpdateStats {
|
pub struct FrameUpdateStats {
|
||||||
pub masks_resized: bool,
|
pub masks_resized: bool,
|
||||||
pub moves_resized: bool,
|
pub moves_resized: bool,
|
||||||
|
pub paints_resized: bool,
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Color, UiRegion, WidgetId,
|
UiRegion, WidgetId,
|
||||||
render::{
|
render::{
|
||||||
ArrBuf,
|
ArrBuf,
|
||||||
data::{MaskIdx, MoveIdx, PrimitiveInstance},
|
data::{MaskIdx, MoveIdx, PrimitiveInstance},
|
||||||
@@ -554,16 +554,18 @@ primitives!(
|
|||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct RectPrimitive {
|
pub struct RectPrimitive {
|
||||||
pub color: Color<u8>,
|
/// Index into the separate paint buffer. Geometry stays untouched when a
|
||||||
|
/// theme replaces the value at this index.
|
||||||
|
pub paint: u32,
|
||||||
pub radius: f32,
|
pub radius: f32,
|
||||||
pub thickness: f32,
|
pub thickness: f32,
|
||||||
pub inner_radius: f32,
|
pub inner_radius: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RectPrimitive {
|
impl RectPrimitive {
|
||||||
pub fn color(color: Color<u8>) -> Self {
|
pub fn color(paint: u32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
color,
|
paint,
|
||||||
radius: 0.0,
|
radius: 0.0,
|
||||||
thickness: 0.0,
|
thickness: 0.0,
|
||||||
inner_radius: 0.0,
|
inner_radius: 0.0,
|
||||||
@@ -580,7 +582,7 @@ pub struct GlyphPrimitive {
|
|||||||
/// not a bind-group or view index, since a page never gets one of its
|
/// not a bind-group or view index, since a page never gets one of its
|
||||||
/// own. See TEXTURES.md's "Recommended shape".
|
/// own. See TEXTURES.md's "Recommended shape".
|
||||||
pub layer: u32,
|
pub layer: u32,
|
||||||
pub color: Color<u8>,
|
pub paint: u32,
|
||||||
pub flags: u32,
|
pub flags: u32,
|
||||||
_pad: u32,
|
_pad: u32,
|
||||||
}
|
}
|
||||||
@@ -588,18 +590,12 @@ pub struct GlyphPrimitive {
|
|||||||
impl GlyphPrimitive {
|
impl GlyphPrimitive {
|
||||||
pub const IS_COLOR: u32 = 1;
|
pub const IS_COLOR: u32 = 1;
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(uv_min: [f32; 2], uv_max: [f32; 2], layer: u32, paint: u32, flags: u32) -> Self {
|
||||||
uv_min: [f32; 2],
|
|
||||||
uv_max: [f32; 2],
|
|
||||||
layer: u32,
|
|
||||||
color: Color<u8>,
|
|
||||||
flags: u32,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
uv_min,
|
uv_min,
|
||||||
uv_max,
|
uv_max,
|
||||||
layer,
|
layer,
|
||||||
color,
|
paint,
|
||||||
flags,
|
flags,
|
||||||
_pad: 0,
|
_pad: 0,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ var<storage> rects: array<Rect>;
|
|||||||
var<storage> glyphs: array<GlyphInfo>;
|
var<storage> glyphs: array<GlyphInfo>;
|
||||||
|
|
||||||
struct Rect {
|
struct Rect {
|
||||||
color: u32,
|
paint: u32,
|
||||||
radius: f32,
|
radius: f32,
|
||||||
thickness: f32,
|
thickness: f32,
|
||||||
inner_radius: f32,
|
inner_radius: f32,
|
||||||
@@ -22,7 +22,7 @@ struct GlyphInfo {
|
|||||||
uv_max: vec2<f32>,
|
uv_max: vec2<f32>,
|
||||||
// A layer in the shared atlas array, not a bind-group index.
|
// A layer in the shared atlas array, not a bind-group index.
|
||||||
layer: u32,
|
layer: u32,
|
||||||
color: u32,
|
paint: u32,
|
||||||
flags: u32,
|
flags: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +64,11 @@ var<storage> move_offsets: array<MoveOffset>;
|
|||||||
// Shared by the vertex stage's drawn primitive and the fragment stage's mask shape.
|
// Shared by the vertex stage's drawn primitive and the fragment stage's mask shape.
|
||||||
@group(3) @binding(2)
|
@group(3) @binding(2)
|
||||||
var<storage> instances: array<PrimitiveInstance>;
|
var<storage> instances: array<PrimitiveInstance>;
|
||||||
|
// Solid linear RGBA today. Primitives already refer to paint records rather
|
||||||
|
// than embedding colours so gradients and texture fills can extend this
|
||||||
|
// lookup without rewriting geometry.
|
||||||
|
@group(3) @binding(3)
|
||||||
|
var<storage> paints: array<vec4<f32>>;
|
||||||
|
|
||||||
// Keep synchronized with render_state.rs. The bound prevents a malformed
|
// Keep synchronized with render_state.rs. The bound prevents a malformed
|
||||||
// parent cycle from hanging the GPU; real widget trees have exceeded 16.
|
// parent cycle from hanging the GPU; real widget trees have exceeded 16.
|
||||||
@@ -222,7 +227,7 @@ fn draw_glyph(region: Region, g: GlyphInfo) -> vec4<f32> {
|
|||||||
if (g.flags & 1u) != 0u {
|
if (g.flags & 1u) != 0u {
|
||||||
return texel;
|
return texel;
|
||||||
}
|
}
|
||||||
var color = unpack4x8unorm(g.color);
|
var color = paints[g.paint];
|
||||||
color.a *= texel.a;
|
color.a *= texel.a;
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
@@ -242,7 +247,7 @@ fn rounded_rect_coverage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
|
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
|
||||||
var color = unpack4x8unorm(rect.color);
|
var color = paints[rect.paint];
|
||||||
|
|
||||||
let edge = 0.5;
|
let edge = 0.5;
|
||||||
|
|
||||||
|
|||||||
@@ -271,7 +271,11 @@ impl GpuTextures {
|
|||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
sample_count: 1,
|
sample_count: 1,
|
||||||
dimension: TextureDimension::D2,
|
dimension: TextureDimension::D2,
|
||||||
format: TextureFormat::Rgba8Unorm,
|
// `image` and swash colour-glyph bytes are encoded sRGB.
|
||||||
|
// Sampling this view decodes RGB to the linear-light values
|
||||||
|
// used by the paint buffer and render pipeline; alpha stays
|
||||||
|
// linear.
|
||||||
|
format: TextureFormat::Rgba8UnormSrgb,
|
||||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
||||||
view_formats: &[],
|
view_formats: &[],
|
||||||
},
|
},
|
||||||
@@ -337,7 +341,10 @@ impl GpuTextures {
|
|||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
sample_count: 1,
|
sample_count: 1,
|
||||||
dimension: TextureDimension::D2,
|
dimension: TextureDimension::D2,
|
||||||
format: TextureFormat::Rgba8Unorm,
|
// One array contains both alpha-mask glyphs and colour glyphs.
|
||||||
|
// sRGB decoding leaves mask pages' white RGB and alpha unchanged
|
||||||
|
// while correctly decoding colour-glyph RGB.
|
||||||
|
format: TextureFormat::Rgba8UnormSrgb,
|
||||||
usage: TextureUsages::TEXTURE_BINDING
|
usage: TextureUsages::TEXTURE_BINDING
|
||||||
| TextureUsages::COPY_DST
|
| TextureUsages::COPY_DST
|
||||||
| TextureUsages::COPY_SRC,
|
| TextureUsages::COPY_SRC,
|
||||||
@@ -430,7 +437,7 @@ pub fn null_texture_view(device: &Device) -> TextureView {
|
|||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
sample_count: 1,
|
sample_count: 1,
|
||||||
dimension: TextureDimension::D2,
|
dimension: TextureDimension::D2,
|
||||||
format: TextureFormat::Rgba8Unorm,
|
format: TextureFormat::Rgba8UnormSrgb,
|
||||||
usage: TextureUsages::TEXTURE_BINDING,
|
usage: TextureUsages::TEXTURE_BINDING,
|
||||||
view_formats: &[],
|
view_formats: &[],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
LayerId, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId, util::Vec2,
|
LayerId, MaskIdx, MoveIdx, PaintId, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId,
|
||||||
|
util::Vec2,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -8,6 +9,10 @@ pub struct ActiveData {
|
|||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
pub parent: Option<WidgetId>,
|
pub parent: Option<WidgetId>,
|
||||||
pub textures: Vec<TextureHandle>,
|
pub textures: Vec<TextureHandle>,
|
||||||
|
/// Paint slots retained by this draw. The GPU primitive stores only the
|
||||||
|
/// slot index, so these handles are what prevent a live primitive from
|
||||||
|
/// observing a recycled paint.
|
||||||
|
pub paints: Vec<PaintId>,
|
||||||
pub primitives: Vec<PrimitiveHandle>,
|
pub primitives: Vec<PrimitiveHandle>,
|
||||||
pub children: Vec<WidgetId>,
|
pub children: Vec<WidgetId>,
|
||||||
pub size_dependencies: Vec<WidgetId>,
|
pub size_dependencies: Vec<WidgetId>,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Mask, MoveOffset, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
Mask, MoveOffset, Paints, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod access;
|
mod access;
|
||||||
@@ -15,6 +15,7 @@ pub use render_state::*;
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct UiData {
|
pub struct UiData {
|
||||||
pub widgets: Widgets,
|
pub widgets: Widgets,
|
||||||
|
pub paints: Paints,
|
||||||
pub textures: Textures,
|
pub textures: Textures,
|
||||||
pub text: TextData,
|
pub text: TextData,
|
||||||
pub masks: TrackedArena<Mask, u32>,
|
pub masks: TrackedArena<Mask, u32>,
|
||||||
@@ -70,5 +71,6 @@ pub trait UiRsc {
|
|||||||
self.on_remove(id);
|
self.on_remove(id);
|
||||||
}
|
}
|
||||||
self.ui_mut().textures.free();
|
self.ui_mut().textures.free();
|
||||||
|
self.ui_mut().paints.free_released();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Axis, Color, Len, MoveOffset, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs,
|
Axis, Len, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs,
|
||||||
TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2,
|
TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2,
|
||||||
WidgetId,
|
WidgetId,
|
||||||
render::{
|
render::{
|
||||||
@@ -20,6 +20,7 @@ pub struct Painter<'a> {
|
|||||||
pub(super) child_move_slot: Option<MoveIdx>,
|
pub(super) child_move_slot: Option<MoveIdx>,
|
||||||
pub(super) own_mask: MaskIdx,
|
pub(super) own_mask: MaskIdx,
|
||||||
pub(super) textures: Vec<TextureHandle>,
|
pub(super) textures: Vec<TextureHandle>,
|
||||||
|
pub(super) paints: Vec<PaintId>,
|
||||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||||
pub(super) recycle: std::iter::Peekable<std::vec::IntoIter<PrimitiveHandle>>,
|
pub(super) recycle: std::iter::Peekable<std::vec::IntoIter<PrimitiveHandle>>,
|
||||||
pub(super) children: Vec<WidgetId>,
|
pub(super) children: Vec<WidgetId>,
|
||||||
@@ -118,6 +119,20 @@ impl<'a> Painter<'a> {
|
|||||||
self.primitive_at(primitive, self.region)
|
self.primitive_at(primitive, self.region)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves a public paint handle to the compact index stored by a GPU
|
||||||
|
/// primitive and retains the handle for exactly as long as that draw.
|
||||||
|
pub fn paint(&mut self, paint: &PaintId) -> u32 {
|
||||||
|
if !self.paints.contains(paint) {
|
||||||
|
self.paints.push(paint.clone());
|
||||||
|
}
|
||||||
|
paint.slot()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paint_value(&mut self, paint: &mut crate::PaintValue) -> u32 {
|
||||||
|
let paint = paint.resolve(&mut self.rsc.ui_mut().paints).clone();
|
||||||
|
self.paint(&paint)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
||||||
self.primitive_at(primitive, region.within(&self.region));
|
self.primitive_at(primitive, region.within(&self.region));
|
||||||
}
|
}
|
||||||
@@ -128,7 +143,8 @@ impl<'a> Painter<'a> {
|
|||||||
/// so keeps pointing at whichever slot it was drawn under. See
|
/// so keeps pointing at whichever slot it was drawn under. See
|
||||||
/// `ActiveData::own_mask` for what pushing a fresh one cost.
|
/// `ActiveData::own_mask` for what pushing a fresh one cost.
|
||||||
pub fn set_mask(&mut self, region: UiRegion) {
|
pub fn set_mask(&mut self, region: UiRegion) {
|
||||||
let shape = self.write_primitive(RectPrimitive::color(Color::NONE), region, Drawn::No);
|
let paint = self.paint(&PaintId::NONE);
|
||||||
|
let shape = self.write_primitive(RectPrimitive::color(paint), region, Drawn::No);
|
||||||
self.set_mask_to(shape);
|
self.set_mask_to(shape);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,6 +449,9 @@ impl<'a> Painter<'a> {
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
for paint in text.paints.iter() {
|
||||||
|
self.paint(paint);
|
||||||
|
}
|
||||||
for glyph in text.glyphs.iter() {
|
for glyph in text.glyphs.iter() {
|
||||||
let mut region = origin;
|
let mut region = origin;
|
||||||
region.x.end = region.x.start;
|
region.x.end = region.x.start;
|
||||||
@@ -445,7 +464,7 @@ impl<'a> Painter<'a> {
|
|||||||
glyph.entry.uv_min,
|
glyph.entry.uv_min,
|
||||||
glyph.entry.uv_max,
|
glyph.entry.uv_max,
|
||||||
glyph.entry.layer,
|
glyph.entry.layer,
|
||||||
glyph.color,
|
glyph.paint,
|
||||||
flags_for(glyph.entry.is_color),
|
flags_for(glyph.entry.is_color),
|
||||||
),
|
),
|
||||||
region,
|
region,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use std::sync::Mutex;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ActiveData, Axis, IdLike, MaskIdx, MoveIdx, Painter, PixelRegion, PrimitiveLayers, RegionAlign,
|
ActiveData, Axis, ChildOrder, IdLike, MaskIdx, MoveIdx, Painter, PixelRegion, PrimitiveLayers,
|
||||||
Size, StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
|
RegionAlign, Size, StrongWidget, UiRegion, UiRsc, UiVec2, Widget, WidgetId, Widgets,
|
||||||
render::{
|
render::{
|
||||||
Drawn, MoveOffset, NOT_DRAWN, Primitive, PrimitiveHandle, PrimitiveInst, Primitives,
|
Drawn, MoveOffset, NOT_DRAWN, Primitive, PrimitiveHandle, PrimitiveInst, Primitives,
|
||||||
RectPrimitive, rounded_rect_coverage,
|
RectPrimitive, rounded_rect_coverage,
|
||||||
@@ -73,6 +73,7 @@ pub(crate) struct Retained {
|
|||||||
pub child_move_slot: Option<MoveIdx>,
|
pub child_move_slot: Option<MoveIdx>,
|
||||||
pub own_mask: MaskIdx,
|
pub own_mask: MaskIdx,
|
||||||
pub primitives: Vec<PrimitiveHandle>,
|
pub primitives: Vec<PrimitiveHandle>,
|
||||||
|
pub paints: Vec<crate::PaintId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Retained {
|
impl Default for Retained {
|
||||||
@@ -84,6 +85,7 @@ impl Default for Retained {
|
|||||||
child_move_slot: None,
|
child_move_slot: None,
|
||||||
own_mask: MaskIdx::NONE,
|
own_mask: MaskIdx::NONE,
|
||||||
primitives: Vec::new(),
|
primitives: Vec::new(),
|
||||||
|
paints: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -350,6 +352,7 @@ impl UiRenderState {
|
|||||||
mut child_move_slot,
|
mut child_move_slot,
|
||||||
mut own_mask,
|
mut own_mask,
|
||||||
primitives: mut recycle,
|
primitives: mut recycle,
|
||||||
|
paints: _old_paints,
|
||||||
} = retained;
|
} = retained;
|
||||||
let dirty = rsc.widgets_mut().needs_redraw.remove(&id);
|
let dirty = rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
let requires_exact_region = rsc
|
let requires_exact_region = rsc
|
||||||
@@ -447,6 +450,7 @@ impl UiRenderState {
|
|||||||
layer,
|
layer,
|
||||||
id,
|
id,
|
||||||
textures: Vec::new(),
|
textures: Vec::new(),
|
||||||
|
paints: Vec::new(),
|
||||||
primitives: Vec::new(),
|
primitives: Vec::new(),
|
||||||
recycle: recycle.into_iter().peekable(),
|
recycle: recycle.into_iter().peekable(),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
@@ -498,6 +502,7 @@ impl UiRenderState {
|
|||||||
child_move_slot,
|
child_move_slot,
|
||||||
own_mask,
|
own_mask,
|
||||||
textures,
|
textures,
|
||||||
|
paints,
|
||||||
primitives,
|
primitives,
|
||||||
recycle,
|
recycle,
|
||||||
children,
|
children,
|
||||||
@@ -517,6 +522,7 @@ impl UiRenderState {
|
|||||||
region,
|
region,
|
||||||
parent,
|
parent,
|
||||||
textures,
|
textures,
|
||||||
|
paints,
|
||||||
primitives,
|
primitives,
|
||||||
children,
|
children,
|
||||||
size_dependencies,
|
size_dependencies,
|
||||||
@@ -1046,6 +1052,41 @@ impl UiRenderState {
|
|||||||
Some(region.to_px(self.output_size))
|
Some(region.to_px(self.output_size))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This widget's immediate rendered children in the order non-visual
|
||||||
|
/// consumers should traverse them. The ordinary case costs no sort and
|
||||||
|
/// exactly preserves draw order; axis-aware layout widgets ask for their
|
||||||
|
/// resolved screen positions only when somebody actually queries them.
|
||||||
|
pub fn ordered_children(&self, id: WidgetId, rsc: &dyn UiRsc) -> Vec<WidgetId> {
|
||||||
|
let Some(active) = self.active.get(&id) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let mut children = active.children.clone();
|
||||||
|
// A layout may draw a child to learn its size and then place that
|
||||||
|
// retained drawing. Both operations are recorded for redraw lifetime,
|
||||||
|
// but a semantic traversal visits the child once.
|
||||||
|
let mut seen = HashSet::default();
|
||||||
|
children.retain(|child| seen.insert(*child));
|
||||||
|
let order = rsc
|
||||||
|
.widgets()
|
||||||
|
.get_dyn(id)
|
||||||
|
.map(Widget::child_order)
|
||||||
|
.unwrap_or_default();
|
||||||
|
if let ChildOrder::Axis(axis) = order {
|
||||||
|
children.sort_by(|a, b| {
|
||||||
|
let at = self
|
||||||
|
.window_region(a, rsc)
|
||||||
|
.map(|r| r.top_left.axis(axis))
|
||||||
|
.unwrap_or(f32::INFINITY);
|
||||||
|
let bt = self
|
||||||
|
.window_region(b, rsc)
|
||||||
|
.map(|r| r.top_left.axis(axis))
|
||||||
|
.unwrap_or(f32::INFINITY);
|
||||||
|
at.total_cmp(&bt)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
children
|
||||||
|
}
|
||||||
|
|
||||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
if self.draw_started.contains(&id) {
|
if self.draw_started.contains(&id) {
|
||||||
@@ -1075,6 +1116,7 @@ impl UiRenderState {
|
|||||||
child_move_slot: active.child_move_slot,
|
child_move_slot: active.child_move_slot,
|
||||||
own_mask: active.own_mask,
|
own_mask: active.own_mask,
|
||||||
primitives: active.primitives,
|
primitives: active.primitives,
|
||||||
|
paints: active.paints,
|
||||||
},
|
},
|
||||||
rsc,
|
rsc,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ pub use tag::*;
|
|||||||
pub use view::*;
|
pub use view::*;
|
||||||
pub use widgets::*;
|
pub use widgets::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum ChildOrder {
|
||||||
|
/// The order in which the parent drew its children.
|
||||||
|
#[default]
|
||||||
|
Draw,
|
||||||
|
/// Ascending visual position on one screen axis. Equal positions keep
|
||||||
|
/// draw order; the resolved coordinates are sorted only when queried.
|
||||||
|
Axis(Axis),
|
||||||
|
}
|
||||||
|
|
||||||
pub trait Widget: Any {
|
pub trait Widget: Any {
|
||||||
fn draw(&mut self, painter: &mut Painter);
|
fn draw(&mut self, painter: &mut Painter);
|
||||||
|
|
||||||
@@ -34,6 +44,10 @@ pub trait Widget: Any {
|
|||||||
accesskit::Role::Unknown
|
accesskit::Role::Unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn child_order(&self) -> ChildOrder {
|
||||||
|
ChildOrder::Draw
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn tick(&mut self, now: std::time::Instant) -> bool {
|
fn tick(&mut self, now: std::time::Instant) -> bool {
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ fn row_image(i: usize) -> image::DynamicImage {
|
|||||||
|
|
||||||
fn build_row<Rsc: UiRsc + 'static>(rsc: &mut Rsc, i: usize) -> StrongWidget {
|
fn build_row<Rsc: UiRsc + 'static>(rsc: &mut Rsc, i: usize) -> StrongWidget {
|
||||||
let tint = if i.is_multiple_of(2) {
|
let tint = if i.is_multiple_of(2) {
|
||||||
Color::rgb(120, 130, 170)
|
Srgba8::rgb(120, 130, 170)
|
||||||
} else {
|
} else {
|
||||||
Color::rgb(70, 80, 140)
|
Srgba8::rgb(70, 80, 140)
|
||||||
};
|
};
|
||||||
let text_color = Color::BLACK;
|
let text_color = PaintId::BLACK;
|
||||||
if i.is_multiple_of(IMAGE_EVERY) {
|
if i.is_multiple_of(IMAGE_EVERY) {
|
||||||
let text = wtext(row_text(i))
|
let text = wtext(row_text(i))
|
||||||
.wrap(true)
|
.wrap(true)
|
||||||
@@ -77,7 +77,7 @@ impl DefaultAppState for State {
|
|||||||
let root = list
|
let root = list
|
||||||
.scrollable()
|
.scrollable()
|
||||||
.masked()
|
.masked()
|
||||||
.background(rect(Color::WHITE))
|
.background(rect(PaintId::WHITE))
|
||||||
.add_strong(rsc);
|
.add_strong(rsc);
|
||||||
ui_state.set_root(root.any());
|
ui_state.set_root(root.any());
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ impl DefaultAppState for State {
|
|||||||
rsc: &mut DefaultRsc<Self>,
|
rsc: &mut DefaultRsc<Self>,
|
||||||
_: Proxy<Self::Event>,
|
_: Proxy<Self::Event>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
rect(Color::RED).set_root(rsc, &mut ui_state);
|
rect(PaintId::RED).set_root(rsc, &mut ui_state);
|
||||||
Self { ui_state }
|
Self { ui_state }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,15 +16,15 @@ impl DefaultAppState for State {
|
|||||||
rsc: &mut DefaultRsc<Self>,
|
rsc: &mut DefaultRsc<Self>,
|
||||||
_: Proxy<Self::Event>,
|
_: Proxy<Self::Event>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let rect = rect(Color::RED).add(rsc);
|
let rect = rect(PaintId::RED).add(rsc);
|
||||||
rect.task_on(CursorSense::click(), async move |mut ctx| {
|
rect.task_on(CursorSense::click(), async move |mut ctx| {
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
ctx.update(move |_, rsc| {
|
ctx.update(move |_, rsc| {
|
||||||
let rect = rect(rsc);
|
let rect = rect(rsc);
|
||||||
if rect.color == Color::RED {
|
if rect.is_paint(&PaintId::RED) {
|
||||||
rect.color = Color::BLUE;
|
rect.set_paint(PaintId::BLUE);
|
||||||
} else {
|
} else {
|
||||||
rect.color = Color::RED;
|
rect.set_paint(PaintId::RED);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ struct Test {
|
|||||||
|
|
||||||
impl Test {
|
impl Test {
|
||||||
pub fn new(rsc: &mut Rsc) -> Self {
|
pub fn new(rsc: &mut Rsc) -> Self {
|
||||||
let root = rect(Color::RED).add(rsc);
|
let root = rect(PaintId::RED).add(rsc);
|
||||||
let cur = rsc.create_state(root, false);
|
let cur = rsc.create_state(root, false);
|
||||||
Self { root, cur }
|
Self { root, cur }
|
||||||
}
|
}
|
||||||
@@ -28,9 +28,9 @@ impl Test {
|
|||||||
let cur = &mut rsc[self.cur];
|
let cur = &mut rsc[self.cur];
|
||||||
*cur = !*cur;
|
*cur = !*cur;
|
||||||
if *cur {
|
if *cur {
|
||||||
rsc[self.root].color = Color::BLUE;
|
rsc[self.root].set_paint(PaintId::BLUE);
|
||||||
} else {
|
} else {
|
||||||
rsc[self.root].color = Color::RED;
|
rsc[self.root].set_paint(PaintId::RED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ fn a_named_widget_reaches_the_tree_with_its_role_and_bounds() {
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let leaf: WeakWidget<Rect> = rect(UiColor::WHITE).label("Add task").add(&mut rsc);
|
let leaf: WeakWidget<Rect> = rect(PaintId::WHITE).label("Add task").add(&mut rsc);
|
||||||
let root = leaf.upgrade(&mut rsc).any();
|
let root = leaf.upgrade(&mut rsc).any();
|
||||||
let mut render = UiRenderState::new();
|
let mut render = UiRenderState::new();
|
||||||
render.resize((800.0, 600.0));
|
render.resize((800.0, 600.0));
|
||||||
@@ -40,7 +40,7 @@ fn a_widget_with_no_label_never_reaches_the_tree() {
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let root = rsc.ui.widgets.add_strong(rect(UiColor::WHITE));
|
let root = rsc.ui.widgets.add_strong(rect(PaintId::WHITE));
|
||||||
let mut render = UiRenderState::new();
|
let mut render = UiRenderState::new();
|
||||||
render.resize((800.0, 600.0));
|
render.resize((800.0, 600.0));
|
||||||
render.update(&root.any(), &mut rsc);
|
render.update(&root.any(), &mut rsc);
|
||||||
@@ -58,7 +58,7 @@ fn bounds_follow_a_moved_widget_and_updates_stay_incremental() {
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let leaf: WeakWidget<Rect> = rect(UiColor::WHITE).label("thing").add(&mut rsc);
|
let leaf: WeakWidget<Rect> = rect(PaintId::WHITE).label("thing").add(&mut rsc);
|
||||||
let leaf_strong = leaf.upgrade(&mut rsc).any();
|
let leaf_strong = leaf.upgrade(&mut rsc).any();
|
||||||
let offset = rsc.ui.widgets.add_strong(Offset {
|
let offset = rsc.ui.widgets.add_strong(Offset {
|
||||||
inner: leaf_strong,
|
inner: leaf_strong,
|
||||||
|
|||||||
+29
-17
@@ -4,7 +4,7 @@ use android_view::{
|
|||||||
jni::{JavaVM, objects::GlobalRef},
|
jni::{JavaVM, objects::GlobalRef},
|
||||||
ndk::native_window::NativeWindow,
|
ndk::native_window::NativeWindow,
|
||||||
};
|
};
|
||||||
use iris_core::{FrameParts, UiData, UiRenderNode, UiRenderState};
|
use iris_core::{FrameParts, LinearRgba, UiData, UiRenderNode, UiRenderState};
|
||||||
use pollster::FutureExt;
|
use pollster::FutureExt;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use wgpu::{
|
use wgpu::{
|
||||||
@@ -12,7 +12,7 @@ use wgpu::{
|
|||||||
*,
|
*,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const CLEAR_COLOR: Color = Color::BLACK;
|
pub const CLEAR_COLOR: LinearRgba = LinearRgba::BLACK;
|
||||||
|
|
||||||
/// `NativeWindow` (from the surface android-view hands over in
|
/// `NativeWindow` (from the surface android-view hands over in
|
||||||
/// `surfaceChanged`) has a window handle but not a display one -- there is
|
/// `surfaceChanged`) has a window handle but not a display one -- there is
|
||||||
@@ -46,6 +46,7 @@ pub struct AndroidRenderer {
|
|||||||
device: Device,
|
device: Device,
|
||||||
queue: Queue,
|
queue: Queue,
|
||||||
config: SurfaceConfiguration,
|
config: SurfaceConfiguration,
|
||||||
|
view_format: TextureFormat,
|
||||||
encoder: CommandEncoder,
|
encoder: CommandEncoder,
|
||||||
pub ui: UiRenderNode,
|
pub ui: UiRenderNode,
|
||||||
pub adapter_name: String,
|
pub adapter_name: String,
|
||||||
@@ -73,6 +74,7 @@ pub struct AndroidRenderer {
|
|||||||
pub struct FrameDiagnostics {
|
pub struct FrameDiagnostics {
|
||||||
pub masks_resized: bool,
|
pub masks_resized: bool,
|
||||||
pub moves_resized: bool,
|
pub moves_resized: bool,
|
||||||
|
pub paints_resized: bool,
|
||||||
pub atlas_pages_grown_prev: u64,
|
pub atlas_pages_grown_prev: u64,
|
||||||
pub image_bind_group_creates_prev: u64,
|
pub image_bind_group_creates_prev: u64,
|
||||||
}
|
}
|
||||||
@@ -189,30 +191,33 @@ impl AndroidRenderer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let surface_caps = surface.get_capabilities(&adapter);
|
let surface_caps = surface.get_capabilities(&adapter);
|
||||||
let surface_format = surface_caps
|
let formats = iris_core::srgb_surface_format(&surface_caps)?;
|
||||||
.formats
|
log::info!(
|
||||||
.iter()
|
"iris renderer: surface={:?}, view={:?}, color_space=Srgb",
|
||||||
.copied()
|
formats.surface,
|
||||||
.find(|f| f.is_srgb())
|
formats.view,
|
||||||
.unwrap_or(surface_caps.formats[0]);
|
);
|
||||||
|
|
||||||
let config = SurfaceConfiguration {
|
let config = SurfaceConfiguration {
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||||
format: surface_format,
|
format: formats.surface,
|
||||||
// wgpu 30's new field; `Auto` is what every earlier version did.
|
// wgpu 30's new field; `Auto` is what every earlier version did.
|
||||||
color_space: SurfaceColorSpace::Auto,
|
color_space: SurfaceColorSpace::Srgb,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
present_mode: PresentMode::AutoVsync,
|
present_mode: PresentMode::AutoVsync,
|
||||||
alpha_mode: surface_caps.alpha_modes[0],
|
alpha_mode: surface_caps.alpha_modes[0],
|
||||||
desired_maximum_frame_latency: 2,
|
desired_maximum_frame_latency: 2,
|
||||||
view_formats: vec![],
|
view_formats: (formats.view != formats.surface)
|
||||||
|
.then_some(formats.view)
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
};
|
};
|
||||||
surface.configure(&device, &config);
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
let encoder = Self::create_encoder(&device);
|
let encoder = Self::create_encoder(&device);
|
||||||
let window_size = iris_core::util::Vec2::new(width as f32, height as f32);
|
let window_size = iris_core::util::Vec2::new(width as f32, height as f32);
|
||||||
let ui = match UiRenderNode::new(&device, &queue, &config, window_size) {
|
let ui = match UiRenderNode::new(&device, &queue, formats.view, window_size) {
|
||||||
Ok(ui) => ui,
|
Ok(ui) => ui,
|
||||||
Err(wgpu_error) => return Err(Self::diagnostic(&adapter, &wgpu_error)),
|
Err(wgpu_error) => return Err(Self::diagnostic(&adapter, &wgpu_error)),
|
||||||
};
|
};
|
||||||
@@ -222,6 +227,7 @@ impl AndroidRenderer {
|
|||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
config,
|
config,
|
||||||
|
view_format: formats.view,
|
||||||
encoder,
|
encoder,
|
||||||
ui,
|
ui,
|
||||||
adapter_name,
|
adapter_name,
|
||||||
@@ -289,8 +295,10 @@ impl AndroidRenderer {
|
|||||||
format!(
|
format!(
|
||||||
"iris diagnostics. Copy this text and send it to Iris.\n\n\
|
"iris diagnostics. Copy this text and send it to Iris.\n\n\
|
||||||
adapter: {name} ({backend:?}), driver: {driver}\n\
|
adapter: {name} ({backend:?}), driver: {driver}\n\
|
||||||
|
surface: {surface:?}, view: {view:?}, color_space: Srgb\n\
|
||||||
content_scale: {content_scale}\n\
|
content_scale: {content_scale}\n\
|
||||||
atlas format: Rgba8Unorm, views live: {views}\n\
|
paint format: linear vec4<f32>\n\
|
||||||
|
atlas/image format: Rgba8UnormSrgb, views live: {views}\n\
|
||||||
fonts: {families_found} families found, default={default_family:?} \
|
fonts: {families_found} families found, default={default_family:?} \
|
||||||
mono={default_mono_family:?}\n\
|
mono={default_mono_family:?}\n\
|
||||||
fonts resolved: regular={regular:?} bold={bold:?} italic={italic:?} \
|
fonts resolved: regular={regular:?} bold={bold:?} italic={italic:?} \
|
||||||
@@ -301,6 +309,8 @@ impl AndroidRenderer {
|
|||||||
name = self.adapter_name,
|
name = self.adapter_name,
|
||||||
backend = self.adapter_backend,
|
backend = self.adapter_backend,
|
||||||
driver = self.adapter_driver,
|
driver = self.adapter_driver,
|
||||||
|
surface = self.config.format,
|
||||||
|
view = self.view_format,
|
||||||
content_scale = self.content_scale,
|
content_scale = self.content_scale,
|
||||||
views = self.ui.view_count(),
|
views = self.ui.view_count(),
|
||||||
families_found = font.families_found,
|
families_found = font.families_found,
|
||||||
@@ -328,6 +338,7 @@ impl AndroidRenderer {
|
|||||||
FrameDiagnostics {
|
FrameDiagnostics {
|
||||||
masks_resized: stats.masks_resized,
|
masks_resized: stats.masks_resized,
|
||||||
moves_resized: stats.moves_resized,
|
moves_resized: stats.moves_resized,
|
||||||
|
paints_resized: stats.paints_resized,
|
||||||
atlas_pages_grown_prev,
|
atlas_pages_grown_prev,
|
||||||
image_bind_group_creates_prev,
|
image_bind_group_creates_prev,
|
||||||
}
|
}
|
||||||
@@ -348,9 +359,10 @@ impl AndroidRenderer {
|
|||||||
other => panic!("no surface texture to draw into: {other:?}"),
|
other => panic!("no surface texture to draw into: {other:?}"),
|
||||||
};
|
};
|
||||||
let acquire = acquire_start.elapsed();
|
let acquire = acquire_start.elapsed();
|
||||||
let view = output
|
let view = output.texture.create_view(&TextureViewDescriptor {
|
||||||
.texture
|
format: Some(self.view_format),
|
||||||
.create_view(&TextureViewDescriptor::default());
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
|
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
|
||||||
{
|
{
|
||||||
@@ -359,7 +371,7 @@ impl AndroidRenderer {
|
|||||||
view: &view,
|
view: &view,
|
||||||
resolve_target: None,
|
resolve_target: None,
|
||||||
ops: Operations {
|
ops: Operations {
|
||||||
load: LoadOp::Clear(CLEAR_COLOR),
|
load: LoadOp::Clear(CLEAR_COLOR.to_wgpu()),
|
||||||
store: StoreOp::Store,
|
store: StoreOp::Store,
|
||||||
},
|
},
|
||||||
depth_slice: None,
|
depth_slice: None,
|
||||||
|
|||||||
@@ -383,10 +383,12 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
|||||||
if renderer.frame_count() <= DIAGNOSTIC_FRAMES {
|
if renderer.frame_count() <= DIAGNOSTIC_FRAMES {
|
||||||
log::info!(
|
log::info!(
|
||||||
"iris frame diagnostics: frame={} masks_resized={} moves_resized={} \
|
"iris frame diagnostics: frame={} masks_resized={} moves_resized={} \
|
||||||
|
paints_resized={} \
|
||||||
atlas_pages_grown_prev={} image_bind_group_creates_prev={} wgpu_errors={}",
|
atlas_pages_grown_prev={} image_bind_group_creates_prev={} wgpu_errors={}",
|
||||||
renderer.frame_count(),
|
renderer.frame_count(),
|
||||||
frame_diagnostics.masks_resized,
|
frame_diagnostics.masks_resized,
|
||||||
frame_diagnostics.moves_resized,
|
frame_diagnostics.moves_resized,
|
||||||
|
frame_diagnostics.paints_resized,
|
||||||
frame_diagnostics.atlas_pages_grown_prev,
|
frame_diagnostics.atlas_pages_grown_prev,
|
||||||
frame_diagnostics.image_bind_group_creates_prev,
|
frame_diagnostics.image_bind_group_creates_prev,
|
||||||
renderer.wgpu_errors.snapshot().len(),
|
renderer.wgpu_errors.snapshot().len(),
|
||||||
@@ -698,6 +700,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
self.rsc.ui.text.atlas.page_count(),
|
self.rsc.ui.text.atlas.page_count(),
|
||||||
);
|
);
|
||||||
self.rsc.ui.textures.reupload();
|
self.rsc.ui.textures.reupload();
|
||||||
|
self.rsc.ui.paints.reupload();
|
||||||
self.state.android_state_mut().renderer = Some(renderer);
|
self.state.android_state_mut().renderer = Some(renderer);
|
||||||
self.render(ctx, Instant::now());
|
self.render(ctx, Instant::now());
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -99,7 +99,7 @@ where
|
|||||||
/// whatever is behind the field (a list to pan) still sees every frame of
|
/// whatever is behind the field (a list to pan) still sees every frame of
|
||||||
/// it, the same as a drag that never touched a selectable field at all.
|
/// it, the same as a drag that never touched a selectable field at all.
|
||||||
fn on_press(
|
fn on_press(
|
||||||
rsc: &mut impl UiRsc,
|
rsc: &mut impl HasEvents,
|
||||||
render: &UiRenderState,
|
render: &UiRenderState,
|
||||||
state: &mut impl FocusHost,
|
state: &mut impl FocusHost,
|
||||||
id: WeakWidget<TextEdit>,
|
id: WeakWidget<TextEdit>,
|
||||||
@@ -107,6 +107,12 @@ fn on_press(
|
|||||||
size: Vec2,
|
size: Vec2,
|
||||||
sense: CursorSense,
|
sense: CursorSense,
|
||||||
) {
|
) {
|
||||||
|
if sense == CursorSense::PressStart(CursorButton::Left) {
|
||||||
|
// An editable field becomes the command destination of the new
|
||||||
|
// interaction. Dismiss a retained display-text selection first so
|
||||||
|
// Copy cannot keep going to text the user has visibly left behind.
|
||||||
|
rsc.run_command(Command::Escape);
|
||||||
|
}
|
||||||
if state.is_focused(id) {
|
if state.is_focused(id) {
|
||||||
// Already focused, so there is no keyboard to withhold -- but a
|
// Already focused, so there is no keyboard to withhold -- but a
|
||||||
// vertical drag still is not a selection. Android's own `EditText`
|
// vertical drag still is not a selection. Android's own `EditText`
|
||||||
|
|||||||
+28
-1
@@ -344,7 +344,34 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
ui_state.window.request_redraw();
|
ui_state.window.request_redraw();
|
||||||
}
|
}
|
||||||
WindowEvent::KeyboardInput { event, .. } => {
|
WindowEvent::KeyboardInput { event, .. } => {
|
||||||
if let Some(sel) = ui_state.focus
|
let requested = event.state.is_pressed().then(|| match &event.logical_key {
|
||||||
|
winit::keyboard::Key::Character(c) if ui_state.input.modifiers.control => {
|
||||||
|
match c.as_str().to_ascii_lowercase().as_str() {
|
||||||
|
"c" => Some(Command::Copy),
|
||||||
|
"a" => Some(Command::SelectAll),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape) => {
|
||||||
|
Some(Command::Escape)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
let command = requested
|
||||||
|
.flatten()
|
||||||
|
.map_or(CommandResult::Unused, |command| rsc.run_command(command));
|
||||||
|
let command_used = match command {
|
||||||
|
CommandResult::Copy(text) => {
|
||||||
|
if let Err(err) = ui_state.clipboard.set_text(text) {
|
||||||
|
eprintln!("failed to copy text to clipboard: {err}")
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
CommandResult::Used => true,
|
||||||
|
CommandResult::Unused => false,
|
||||||
|
};
|
||||||
|
if !command_used
|
||||||
|
&& let Some(sel) = ui_state.focus
|
||||||
&& event.state.is_pressed()
|
&& event.state.is_pressed()
|
||||||
{
|
{
|
||||||
let mut text = sel.edit(rsc);
|
let mut text = sel.edit(rsc);
|
||||||
|
|||||||
+23
-16
@@ -1,12 +1,12 @@
|
|||||||
use crate::task::RequestRedraw;
|
use crate::task::RequestRedraw;
|
||||||
use iris_core::{FrameParts, UiData, UiRenderNode, UiRenderState, util::Vec2};
|
use iris_core::{FrameParts, LinearRgba, UiData, UiRenderNode, UiRenderState, util::Vec2};
|
||||||
use pollster::FutureExt;
|
use pollster::FutureExt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use wgpu::*;
|
use wgpu::*;
|
||||||
use winit::{dpi::PhysicalSize, window::Window};
|
use winit::{dpi::PhysicalSize, window::Window};
|
||||||
|
|
||||||
pub const CLEAR_COLOR: Color = Color::BLACK;
|
pub const CLEAR_COLOR: LinearRgba = LinearRgba::BLACK;
|
||||||
|
|
||||||
impl RequestRedraw for Window {
|
impl RequestRedraw for Window {
|
||||||
fn request_redraw(&self) {
|
fn request_redraw(&self) {
|
||||||
@@ -20,6 +20,7 @@ pub struct UiRenderer {
|
|||||||
device: Device,
|
device: Device,
|
||||||
queue: Queue,
|
queue: Queue,
|
||||||
config: SurfaceConfiguration,
|
config: SurfaceConfiguration,
|
||||||
|
view_format: TextureFormat,
|
||||||
encoder: CommandEncoder,
|
encoder: CommandEncoder,
|
||||||
pub ui: UiRenderNode,
|
pub ui: UiRenderNode,
|
||||||
}
|
}
|
||||||
@@ -45,9 +46,10 @@ impl UiRenderer {
|
|||||||
other => panic!("no surface texture to draw into: {other:?}"),
|
other => panic!("no surface texture to draw into: {other:?}"),
|
||||||
};
|
};
|
||||||
let acquire = acquire_start.elapsed();
|
let acquire = acquire_start.elapsed();
|
||||||
let view = output
|
let view = output.texture.create_view(&TextureViewDescriptor {
|
||||||
.texture
|
format: Some(self.view_format),
|
||||||
.create_view(&TextureViewDescriptor::default());
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
|
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
|
||||||
{
|
{
|
||||||
@@ -56,7 +58,7 @@ impl UiRenderer {
|
|||||||
view: &view,
|
view: &view,
|
||||||
resolve_target: None,
|
resolve_target: None,
|
||||||
ops: Operations {
|
ops: Operations {
|
||||||
load: LoadOp::Clear(CLEAR_COLOR),
|
load: LoadOp::Clear(CLEAR_COLOR.to_wgpu()),
|
||||||
store: StoreOp::Store,
|
store: StoreOp::Store,
|
||||||
},
|
},
|
||||||
depth_slice: None,
|
depth_slice: None,
|
||||||
@@ -170,18 +172,19 @@ impl UiRenderer {
|
|||||||
.expect("Could not get device!");
|
.expect("Could not get device!");
|
||||||
|
|
||||||
let surface_caps = surface.get_capabilities(&adapter);
|
let surface_caps = surface.get_capabilities(&adapter);
|
||||||
let surface_format = surface_caps
|
let formats = iris_core::srgb_surface_format(&surface_caps)
|
||||||
.formats
|
.expect("Could not select an sRGB iris surface format");
|
||||||
.iter()
|
log::info!(
|
||||||
.copied()
|
"iris renderer: surface={:?}, view={:?}, color_space=Srgb",
|
||||||
.find(|f| f.is_srgb())
|
formats.surface,
|
||||||
.unwrap_or(surface_caps.formats[0]);
|
formats.view,
|
||||||
|
);
|
||||||
|
|
||||||
let config = SurfaceConfiguration {
|
let config = SurfaceConfiguration {
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||||
format: surface_format,
|
format: formats.surface,
|
||||||
// wgpu 30's new field; `Auto` is what every earlier version did.
|
// wgpu 30's new field; `Auto` is what every earlier version did.
|
||||||
color_space: SurfaceColorSpace::Auto,
|
color_space: SurfaceColorSpace::Srgb,
|
||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
// Vsync, because a toolkit aiming at battery life must not present
|
// Vsync, because a toolkit aiming at battery life must not present
|
||||||
@@ -192,7 +195,10 @@ impl UiRenderer {
|
|||||||
present_mode: PresentMode::AutoVsync,
|
present_mode: PresentMode::AutoVsync,
|
||||||
alpha_mode: surface_caps.alpha_modes[0],
|
alpha_mode: surface_caps.alpha_modes[0],
|
||||||
desired_maximum_frame_latency: 2,
|
desired_maximum_frame_latency: 2,
|
||||||
view_formats: vec![],
|
view_formats: (formats.view != formats.surface)
|
||||||
|
.then_some(formats.view)
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
surface.configure(&device, &config);
|
surface.configure(&device, &config);
|
||||||
@@ -210,7 +216,7 @@ impl UiRenderer {
|
|||||||
// `default::content_scale` for why this backend stopped dividing
|
// `default::content_scale` for why this backend stopped dividing
|
||||||
// into a separate logical space, and what disagreed while it did.
|
// into a separate logical space, and what disagreed while it did.
|
||||||
let physical_size = Vec2::new(size.width as f32, size.height as f32);
|
let physical_size = Vec2::new(size.width as f32, size.height as f32);
|
||||||
let ui = UiRenderNode::new(&device, &queue, &config, physical_size)
|
let ui = UiRenderNode::new(&device, &queue, formats.view, physical_size)
|
||||||
.expect("Could not create iris render node!");
|
.expect("Could not create iris render node!");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -218,6 +224,7 @@ impl UiRenderer {
|
|||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
config,
|
config,
|
||||||
|
view_format: formats.view,
|
||||||
encoder,
|
encoder,
|
||||||
ui,
|
ui,
|
||||||
window,
|
window,
|
||||||
|
|||||||
@@ -36,6 +36,17 @@ pub trait Eventable<Rsc: HasEvents, Tag>: WidgetLike<Rsc, Tag> {
|
|||||||
}
|
}
|
||||||
impl<WL: WidgetLike<Rsc, Tag>, Rsc: HasEvents, Tag> Eventable<Rsc, Tag> for WL {}
|
impl<WL: WidgetLike<Rsc, Tag>, Rsc: HasEvents, Tag> Eventable<Rsc, Tag> for WL {}
|
||||||
|
|
||||||
|
pub trait Controllable<Rsc: HasEvents, Tag>: WidgetLike<Rsc, Tag> {
|
||||||
|
fn controller<C: Controller<Rsc>>(self, controller: C) -> impl WidgetIdFn<Rsc, Self::Widget> {
|
||||||
|
move |rsc| {
|
||||||
|
let id = self.add(rsc);
|
||||||
|
rsc.register_controller(id, controller);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<WL: WidgetLike<Rsc, Tag>, Rsc: HasEvents, Tag> Controllable<Rsc, Tag> for WL {}
|
||||||
|
|
||||||
widget_trait! {
|
widget_trait! {
|
||||||
pub trait TaskEventable<Rsc: HasEvents + HasTasks>;
|
pub trait TaskEventable<Rsc: HasEvents + HasTasks>;
|
||||||
fn task_on<E: EventLike, F: AsyncWidgetEventFn<Rsc, WL::Widget>>(
|
fn task_on<E: EventLike, F: AsyncWidgetEventFn<Rsc, WL::Widget>>(
|
||||||
|
|||||||
+28
-27
@@ -19,8 +19,9 @@ struct FixedRect(f32);
|
|||||||
impl Widget for FixedRect {
|
impl Widget for FixedRect {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
let size = Size::from_axis(Axis::Y, Len::abs(self.0), Len::REST);
|
let size = Size::from_axis(Axis::Y, Len::abs(self.0), Len::REST);
|
||||||
|
let paint = painter.paint(&PaintId::WHITE);
|
||||||
painter.primitive_within(
|
painter.primitive_within(
|
||||||
RectPrimitive::color(UiColor::WHITE),
|
RectPrimitive::color(paint),
|
||||||
size.to_uivec2(painter.density())
|
size.to_uivec2(painter.density())
|
||||||
.align(RegionAlign::TOP_LEFT),
|
.align(RegionAlign::TOP_LEFT),
|
||||||
);
|
);
|
||||||
@@ -89,8 +90,8 @@ fn a_widget_retains_its_entry_layer_not_its_child_cursor() {
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let back = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let back = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let front = rsc.ui.widgets.add_strong(Rect::new(UiColor::RED));
|
let front = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED));
|
||||||
let stack = rsc.ui.widgets.add_strong(Stack {
|
let stack = rsc.ui.widgets.add_strong(Stack {
|
||||||
children: vec![back.any(), front.any()],
|
children: vec![back.any(), front.any()],
|
||||||
size: StackSize::Default,
|
size: StackSize::Default,
|
||||||
@@ -277,7 +278,7 @@ fn a_hinted_rest_draws_once_and_only_moves_the_fixed_child_after_it() {
|
|||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let first = rsc.ui.widgets.add_strong(FixedRect(40.0));
|
let first = rsc.ui.widgets.add_strong(FixedRect(40.0));
|
||||||
let fill = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let fill = rsc.ui.widgets.add_strong(Sized {
|
let fill = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: fill.any(),
|
inner: fill.any(),
|
||||||
x: None,
|
x: None,
|
||||||
@@ -309,7 +310,7 @@ fn scrolled_rects(
|
|||||||
let mut span = Span::empty(Dir::DOWN);
|
let mut span = Span::empty(Dir::DOWN);
|
||||||
let mut rects = Vec::with_capacity(n);
|
let mut rects = Vec::with_capacity(n);
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
rects.push(rect.weak());
|
rects.push(rect.weak());
|
||||||
let row = rsc.ui.widgets.add_strong(Sized {
|
let row = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: rect.any(),
|
inner: rect.any(),
|
||||||
@@ -459,13 +460,13 @@ fn composer_like_tree(rsc: &mut TestRsc) -> (WeakWidget<TextEdit>, StrongWidget)
|
|||||||
.text_align(Align::LEFT)
|
.text_align(Align::LEFT)
|
||||||
.wrap(true)
|
.wrap(true)
|
||||||
.size(18)
|
.size(18)
|
||||||
.color(UiColor::WHITE)
|
.color(PaintId::WHITE)
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
let bar = (field.pad(dp(12)).width(rest(1)),)
|
let bar = (field.pad(dp(12)).width(rest(1)),)
|
||||||
.span(Dir::RIGHT)
|
.span(Dir::RIGHT)
|
||||||
.background(rect(UiColor::new(40, 40, 46, 255)))
|
.background(rect(Srgba8::new(40, 40, 46, 255)))
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
let list_stand_in = rect(UiColor::BLACK).height(rest(1)).add(rsc);
|
let list_stand_in = rect(PaintId::BLACK).height(rest(1)).add(rsc);
|
||||||
let tree = (list_stand_in, bar).span(Dir::DOWN).add_strong(rsc).any();
|
let tree = (list_stand_in, bar).span(Dir::DOWN).add_strong(rsc).any();
|
||||||
(field, tree)
|
(field, tree)
|
||||||
}
|
}
|
||||||
@@ -517,7 +518,7 @@ fn a_scroll_measures_the_box_it_was_offered_not_the_window() {
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let tall = rsc.ui.widgets.add_strong(Sized {
|
let tall = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: rect.any(),
|
inner: rect.any(),
|
||||||
x: None,
|
x: None,
|
||||||
@@ -567,7 +568,7 @@ fn a_panned_widgets_own_hit_box_moves_exactly_once() {
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let tall = rsc.ui.widgets.add_strong(Sized {
|
let tall = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: rect.any(),
|
inner: rect.any(),
|
||||||
x: None,
|
x: None,
|
||||||
@@ -609,7 +610,7 @@ fn a_masked_widget_keeps_one_mask_slot_that_is_always_its_own_region() {
|
|||||||
inner: inner_root,
|
inner: inner_root,
|
||||||
});
|
});
|
||||||
let masked_id = masked.id();
|
let masked_id = masked.id();
|
||||||
let filler = rsc.ui.widgets.add_strong(Rect::new(UiColor::BLACK));
|
let filler = rsc.ui.widgets.add_strong(Rect::new(PaintId::BLACK));
|
||||||
let filler = rsc.ui.widgets.add_strong(Sized {
|
let filler = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: filler.any(),
|
inner: filler.any(),
|
||||||
x: None,
|
x: None,
|
||||||
@@ -650,7 +651,7 @@ fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() {
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let tall = rsc.ui.widgets.add_strong(Sized {
|
let tall = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: rect.any(),
|
inner: rect.any(),
|
||||||
x: None,
|
x: None,
|
||||||
@@ -662,7 +663,7 @@ fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() {
|
|||||||
y: Some(Len::dp(100.0)),
|
y: Some(Len::dp(100.0)),
|
||||||
});
|
});
|
||||||
let capped_w = capped.weak();
|
let capped_w = capped.weak();
|
||||||
let filler = rsc.ui.widgets.add_strong(Rect::new(UiColor::BLACK));
|
let filler = rsc.ui.widgets.add_strong(Rect::new(PaintId::BLACK));
|
||||||
let filler = rsc.ui.widgets.add_strong(Sized {
|
let filler = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: filler.any(),
|
inner: filler.any(),
|
||||||
x: None,
|
x: None,
|
||||||
@@ -692,14 +693,14 @@ fn a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at(
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let top = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let top = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let spacer = rsc.ui.widgets.add_strong(Sized {
|
let spacer = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: top.any(),
|
inner: top.any(),
|
||||||
x: None,
|
x: None,
|
||||||
y: Some(Len::abs(100.0)),
|
y: Some(Len::abs(100.0)),
|
||||||
});
|
});
|
||||||
let spacer_w = spacer.weak();
|
let spacer_w = spacer.weak();
|
||||||
let below = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let below = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let below_w = below.weak();
|
let below_w = below.weak();
|
||||||
let mut span = Span::empty(Dir::DOWN);
|
let mut span = Span::empty(Dir::DOWN);
|
||||||
span.push(spacer.any());
|
span.push(spacer.any());
|
||||||
@@ -759,7 +760,7 @@ fn a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let child = rsc.ui.widgets.add_strong(Sized {
|
let child = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: rect.any(),
|
inner: rect.any(),
|
||||||
x: None,
|
x: None,
|
||||||
@@ -800,12 +801,12 @@ fn a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement
|
|||||||
const RADIUS: f32 = 20.0;
|
const RADIUS: f32 = 20.0;
|
||||||
|
|
||||||
fn rounded_container(rsc: &mut TestRsc) -> (UiRenderState, MaskIdx, WidgetId, u32) {
|
fn rounded_container(rsc: &mut TestRsc) -> (UiRenderState, MaskIdx, WidgetId, u32) {
|
||||||
let child = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let child = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let child_id = child.id();
|
let child_id = child.id();
|
||||||
let shape = rsc
|
let shape = rsc
|
||||||
.ui
|
.ui
|
||||||
.widgets
|
.widgets
|
||||||
.add_strong(Rect::new(UiColor::BLACK).radius(Len::abs(RADIUS)));
|
.add_strong(Rect::new(PaintId::BLACK).radius(Len::abs(RADIUS)));
|
||||||
let shape_id = shape.id();
|
let shape_id = shape.id();
|
||||||
let root = rsc
|
let root = rsc
|
||||||
.ui
|
.ui
|
||||||
@@ -909,13 +910,13 @@ fn nested_masks_multiply_their_coverage() {
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let child = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let child = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let child_id = child.id();
|
let child_id = child.id();
|
||||||
|
|
||||||
let inner_shape = rsc
|
let inner_shape = rsc
|
||||||
.ui
|
.ui
|
||||||
.widgets
|
.widgets
|
||||||
.add_strong(Rect::new(UiColor::BLACK).radius(Len::abs(RADIUS)));
|
.add_strong(Rect::new(PaintId::BLACK).radius(Len::abs(RADIUS)));
|
||||||
let inner_shape_id = inner_shape.id();
|
let inner_shape_id = inner_shape.id();
|
||||||
let inner = rsc.ui.widgets.add_strong(Masked {
|
let inner = rsc.ui.widgets.add_strong(Masked {
|
||||||
shape: Some(inner_shape.any()),
|
shape: Some(inner_shape.any()),
|
||||||
@@ -924,7 +925,7 @@ fn nested_masks_multiply_their_coverage() {
|
|||||||
let outer_shape = rsc
|
let outer_shape = rsc
|
||||||
.ui
|
.ui
|
||||||
.widgets
|
.widgets
|
||||||
.add_strong(Rect::new(UiColor::BLACK).radius(Len::abs(RADIUS)));
|
.add_strong(Rect::new(PaintId::BLACK).radius(Len::abs(RADIUS)));
|
||||||
let outer_shape_id = outer_shape.id();
|
let outer_shape_id = outer_shape.id();
|
||||||
let root = rsc
|
let root = rsc
|
||||||
.ui
|
.ui
|
||||||
@@ -1004,7 +1005,7 @@ fn a_scroll_area_opens_at_the_start_of_content_it_has_not_measured_yet() {
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let fill = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE)).any();
|
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any();
|
||||||
let tall = rsc.ui.widgets.add_strong(Sized {
|
let tall = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: fill,
|
inner: fill,
|
||||||
x: None,
|
x: None,
|
||||||
@@ -1039,7 +1040,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() {
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let header_fill = rsc.ui.widgets.add_strong(Rect::new(UiColor::RED)).any();
|
let header_fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED)).any();
|
||||||
let header_id = header_fill.id();
|
let header_id = header_fill.id();
|
||||||
let header = rsc.ui.widgets.add_strong(Sized {
|
let header = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: header_fill,
|
inner: header_fill,
|
||||||
@@ -1049,7 +1050,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() {
|
|||||||
let mut inner = Span::empty(Dir::DOWN);
|
let mut inner = Span::empty(Dir::DOWN);
|
||||||
let mut rects = Vec::new();
|
let mut rects = Vec::new();
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
rects.push(rect.weak());
|
rects.push(rect.weak());
|
||||||
let sized = rsc.ui.widgets.add_strong(Sized {
|
let sized = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: rect.any(),
|
inner: rect.any(),
|
||||||
@@ -1061,7 +1062,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() {
|
|||||||
inner: sized.any(),
|
inner: sized.any(),
|
||||||
exact_region: false,
|
exact_region: false,
|
||||||
});
|
});
|
||||||
let fill = rsc.ui.widgets.add_strong(Rect::new(UiColor::BLUE)).any();
|
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::BLUE)).any();
|
||||||
let card = rsc.ui.widgets.add_strong(Stack {
|
let card = rsc.ui.widgets.add_strong(Stack {
|
||||||
children: vec![fill, padded.any()],
|
children: vec![fill, padded.any()],
|
||||||
size: StackSize::Child(1),
|
size: StackSize::Child(1),
|
||||||
@@ -1122,7 +1123,7 @@ fn a_new_child_in_a_growing_lazy_row_uses_its_final_box_immediately() {
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let first = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let first = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let first_id = first.id();
|
let first_id = first.id();
|
||||||
let first = rsc.ui.widgets.add_strong(Sized {
|
let first = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: first.any(),
|
inner: first.any(),
|
||||||
@@ -1146,7 +1147,7 @@ fn a_new_child_in_a_growing_lazy_row_uses_its_final_box_immediately() {
|
|||||||
render.resize((200.0, 200.0));
|
render.resize((200.0, 200.0));
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
|
|
||||||
let second = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let second = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let second_id = second.id();
|
let second_id = second.id();
|
||||||
let second = rsc.ui.widgets.add_strong(Sized {
|
let second = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: second.any(),
|
inner: second.any(),
|
||||||
|
|||||||
+1
-1
@@ -1986,7 +1986,7 @@ mod drag_gesture_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn some_id(ui: &mut UiData) -> WidgetId {
|
fn some_id(ui: &mut UiData) -> WidgetId {
|
||||||
ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id()
|
ui.widgets.add_strong(Rect::new(PaintId::WHITE)).id()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+10
-10
@@ -55,9 +55,9 @@ fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// the case in IRIS_TODO.md's report.
|
// the case in IRIS_TODO.md's report.
|
||||||
let list = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let list = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let list_weak = list.weak();
|
let list_weak = list.weak();
|
||||||
let button = rsc.ui.widgets.add_strong(Rect::new(UiColor::RED));
|
let button = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED));
|
||||||
let button_weak = button.weak();
|
let button_weak = button.weak();
|
||||||
|
|
||||||
let scrolled = Rc::new(Cell::new(false));
|
let scrolled = Rc::new(Cell::new(false));
|
||||||
@@ -125,7 +125,7 @@ fn a_release_outside_every_hit_region_still_reaches_the_captured_widget() {
|
|||||||
events: EventManager::default(),
|
events: EventManager::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let draggable = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE)).any();
|
let draggable = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any();
|
||||||
let draggable_weak = draggable.weak();
|
let draggable_weak = draggable.weak();
|
||||||
|
|
||||||
let dropped = Rc::new(Cell::new(false));
|
let dropped = Rc::new(Cell::new(false));
|
||||||
@@ -184,9 +184,9 @@ fn capturing_one_widget_starves_every_other_widget_of_events() {
|
|||||||
events: EventManager::default(),
|
events: EventManager::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let a = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let a = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let a_weak = a.weak();
|
let a_weak = a.weak();
|
||||||
let b = rsc.ui.widgets.add_strong(Rect::new(UiColor::RED));
|
let b = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED));
|
||||||
let b_weak = b.weak();
|
let b_weak = b.weak();
|
||||||
|
|
||||||
let b_hovered = Rc::new(Cell::new(false));
|
let b_hovered = Rc::new(Cell::new(false));
|
||||||
@@ -229,7 +229,7 @@ fn a_finger_drag_over_a_scroll_area_pans_it() {
|
|||||||
events: EventManager::default(),
|
events: EventManager::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let scroll_strong = rect(UiColor::WHITE)
|
let scroll_strong = rect(PaintId::WHITE)
|
||||||
.height(Len::abs(1000.0))
|
.height(Len::abs(1000.0))
|
||||||
.scrollable(Axis::Y, Pin::Start)
|
.scrollable(Axis::Y, Pin::Start)
|
||||||
.add_strong(&mut rsc);
|
.add_strong(&mut rsc);
|
||||||
@@ -319,7 +319,7 @@ fn a_scroll_area_that_captured_the_pointer_learns_its_gesture_ended() {
|
|||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
events: EventManager::default(),
|
events: EventManager::default(),
|
||||||
};
|
};
|
||||||
let scroll_strong = rect(UiColor::WHITE)
|
let scroll_strong = rect(PaintId::WHITE)
|
||||||
.height(Len::abs(1000.0))
|
.height(Len::abs(1000.0))
|
||||||
.scrollable(Axis::Y, Pin::Start)
|
.scrollable(Axis::Y, Pin::Start)
|
||||||
.add_strong(&mut rsc);
|
.add_strong(&mut rsc);
|
||||||
@@ -374,7 +374,7 @@ fn taking_the_pointer_cancels_everyone_else_tracking_the_press() {
|
|||||||
events: EventManager::default(),
|
events: EventManager::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let capturer = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let capturer = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let capturer_weak = capturer.weak();
|
let capturer_weak = capturer.weak();
|
||||||
let bystander = rsc.ui.widgets.add_strong(Stack {
|
let bystander = rsc.ui.widgets.add_strong(Stack {
|
||||||
children: vec![capturer.any()],
|
children: vec![capturer.any()],
|
||||||
@@ -474,7 +474,7 @@ fn a_drag_pans_whichever_nested_scroll_area_owns_its_axis() {
|
|||||||
};
|
};
|
||||||
let seen = Rc::new(Cell::new(None));
|
let seen = Rc::new(Cell::new(None));
|
||||||
let record = seen.clone();
|
let record = seen.clone();
|
||||||
let outer_strong = rect(UiColor::WHITE)
|
let outer_strong = rect(PaintId::WHITE)
|
||||||
.width(Len::abs(1000.0))
|
.width(Len::abs(1000.0))
|
||||||
.height(Len::abs(1000.0))
|
.height(Len::abs(1000.0))
|
||||||
.scrollable(Axis::X, Pin::Start)
|
.scrollable(Axis::X, Pin::Start)
|
||||||
@@ -533,7 +533,7 @@ fn a_press_does_not_reach_a_widget_the_pointer_has_just_left() {
|
|||||||
let seen: [Rc<Cell<Option<WeakWidget<ScrollArea>>>>; 2] = Default::default();
|
let seen: [Rc<Cell<Option<WeakWidget<ScrollArea>>>>; 2] = Default::default();
|
||||||
let half = |slot: &Rc<Cell<Option<WeakWidget<ScrollArea>>>>| {
|
let half = |slot: &Rc<Cell<Option<WeakWidget<ScrollArea>>>>| {
|
||||||
let record = slot.clone();
|
let record = slot.clone();
|
||||||
rect(UiColor::WHITE)
|
rect(PaintId::WHITE)
|
||||||
.height(Len::abs(1000.0))
|
.height(Len::abs(1000.0))
|
||||||
.scrollable(Axis::Y, Pin::Start)
|
.scrollable(Axis::Y, Pin::Start)
|
||||||
.with_id(move |_rsc, id| {
|
.with_id(move |_rsc, id| {
|
||||||
|
|||||||
@@ -741,6 +741,10 @@ impl Scrollable for LazySpan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for LazySpan {
|
impl Widget for LazySpan {
|
||||||
|
fn child_order(&self) -> ChildOrder {
|
||||||
|
ChildOrder::Axis(self.dir.axis)
|
||||||
|
}
|
||||||
|
|
||||||
/// A lazy span animates exactly one thing, its fling -- and it drives
|
/// A lazy span animates exactly one thing, its fling -- and it drives
|
||||||
/// its own rather than being handed deltas by a `ScrollArea` around
|
/// its own rather than being handed deltas by a `ScrollArea` around
|
||||||
/// it, since which rows exist at all is a function of where it is
|
/// it, since which rows exist at all is a function of where it is
|
||||||
@@ -842,7 +846,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn fixed_row(rsc: &mut TestRsc, height: f32) -> (WeakWidget<Sized>, StrongWidget) {
|
fn fixed_row(rsc: &mut TestRsc, height: f32) -> (WeakWidget<Sized>, StrongWidget) {
|
||||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let sized = rsc.ui.widgets.add_strong(Sized {
|
let sized = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: rect.any(),
|
inner: rect.any(),
|
||||||
x: None,
|
x: None,
|
||||||
@@ -1054,9 +1058,9 @@ mod tests {
|
|||||||
rsc: &mut TestRsc,
|
rsc: &mut TestRsc,
|
||||||
height: f32,
|
height: f32,
|
||||||
) -> (WidgetId, WeakWidget<Sized>, StrongWidget) {
|
) -> (WidgetId, WeakWidget<Sized>, StrongWidget) {
|
||||||
let bg = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let bg = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let bg_id = bg.id();
|
let bg_id = bg.id();
|
||||||
let fg_rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
let fg_rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||||
let fg = rsc.ui.widgets.add_strong(Sized {
|
let fg = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: fg_rect.any(),
|
inner: fg_rect.any(),
|
||||||
x: None,
|
x: None,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ mod tests {
|
|||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let fill = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE)).any();
|
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any();
|
||||||
let id = fill.id();
|
let id = fill.id();
|
||||||
let long = Some(Len::abs(1000.0));
|
let long = Some(Len::abs(1000.0));
|
||||||
let tall = rsc.ui.widgets.add_strong(Sized {
|
let tall = rsc.ui.widgets.add_strong(Sized {
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ pub struct Span {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Span {
|
impl Widget for Span {
|
||||||
|
fn child_order(&self) -> ChildOrder {
|
||||||
|
ChildOrder::Axis(self.dir.axis)
|
||||||
|
}
|
||||||
|
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
let axis = self.dir.axis;
|
let axis = self.dir.axis;
|
||||||
let gap = self.gap.apply_rest(painter.density()).abs;
|
let gap = self.gap.apply_rest(painter.density()).abs;
|
||||||
|
|||||||
+20
-9
@@ -1,26 +1,36 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone)]
|
||||||
pub struct Rect {
|
pub struct Rect {
|
||||||
pub color: UiColor,
|
paint: PaintValue,
|
||||||
pub radius: Len,
|
pub radius: Len,
|
||||||
pub thickness: f32,
|
pub thickness: f32,
|
||||||
pub inner_radius: f32,
|
pub inner_radius: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rect {
|
impl Rect {
|
||||||
pub fn new(color: UiColor) -> Self {
|
pub fn new(paint: impl Paint) -> Self {
|
||||||
Self {
|
Self {
|
||||||
color,
|
paint: paint.into_value(),
|
||||||
radius: Len::ZERO,
|
radius: Len::ZERO,
|
||||||
inner_radius: 0.0,
|
inner_radius: 0.0,
|
||||||
thickness: 0.0,
|
thickness: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn color(mut self, color: UiColor) -> Self {
|
pub fn paint(mut self, paint: impl Paint) -> Self {
|
||||||
self.color = color;
|
self.paint = paint.into_value();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
pub fn color(self, paint: impl Paint) -> Self {
|
||||||
|
self.paint(paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_paint(&mut self, paint: impl Paint) {
|
||||||
|
self.paint = paint.into_value();
|
||||||
|
}
|
||||||
|
pub fn is_paint(&self, paint: &PaintId) -> bool {
|
||||||
|
self.paint.is(paint)
|
||||||
|
}
|
||||||
pub fn radius(mut self, radius: impl Into<Len>) -> Self {
|
pub fn radius(mut self, radius: impl Into<Len>) -> Self {
|
||||||
self.radius = radius.into();
|
self.radius = radius.into();
|
||||||
self
|
self
|
||||||
@@ -29,8 +39,9 @@ impl Rect {
|
|||||||
|
|
||||||
impl Widget for Rect {
|
impl Widget for Rect {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
|
let paint = painter.paint_value(&mut self.paint);
|
||||||
painter.primitive(RectPrimitive {
|
painter.primitive(RectPrimitive {
|
||||||
color: self.color,
|
paint,
|
||||||
radius: self.radius.fold_dp(painter.density()).abs,
|
radius: self.radius.fold_dp(painter.density()).abs,
|
||||||
thickness: self.thickness,
|
thickness: self.thickness,
|
||||||
inner_radius: self.inner_radius,
|
inner_radius: self.inner_radius,
|
||||||
@@ -43,6 +54,6 @@ impl Widget for Rect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rect(color: UiColor) -> Rect {
|
pub fn rect(paint: impl Paint) -> Rect {
|
||||||
Rect::new(color)
|
Rect::new(paint)
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ impl<State, O, H: WidgetOption<State>> TextBuilder<State, O, H> {
|
|||||||
self.attrs.line_height = self.attrs.font_size * LINE_HEIGHT_MULT;
|
self.attrs.line_height = self.attrs.font_size * LINE_HEIGHT_MULT;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub fn color(mut self, color: UiColor) -> Self {
|
pub fn color(mut self, color: PaintId) -> Self {
|
||||||
self.attrs.color = color;
|
self.attrs.color = color;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-123
@@ -1,7 +1,9 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use iris_core::{TextData, UiColor};
|
use iris_core::{PaintId, TextData};
|
||||||
use parley::{Affinity, Layout, Selection};
|
use parley::{Affinity, Layout, Selection};
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use super::selection_layout;
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
use winit::{
|
use winit::{
|
||||||
event::KeyEvent,
|
event::KeyEvent,
|
||||||
@@ -22,10 +24,8 @@ pub enum Motion {
|
|||||||
|
|
||||||
pub struct TextEdit {
|
pub struct TextEdit {
|
||||||
view: TextView,
|
view: TextView,
|
||||||
selection: Option<Selection>,
|
|
||||||
#[cfg_attr(target_os = "android", allow(dead_code))]
|
#[cfg_attr(target_os = "android", allow(dead_code))]
|
||||||
history: Vec<(String, Option<Selection>)>,
|
history: Vec<(String, Option<Selection>)>,
|
||||||
double_hit: Option<usize>,
|
|
||||||
pub(crate) press_origin: Option<Vec2>,
|
pub(crate) press_origin: Option<Vec2>,
|
||||||
pub mode: EditMode,
|
pub mode: EditMode,
|
||||||
}
|
}
|
||||||
@@ -40,20 +40,14 @@ impl TextEdit {
|
|||||||
pub fn new(view: TextView, mode: EditMode) -> Self {
|
pub fn new(view: TextView, mode: EditMode) -> Self {
|
||||||
Self {
|
Self {
|
||||||
view,
|
view,
|
||||||
selection: None,
|
|
||||||
history: Default::default(),
|
history: Default::default(),
|
||||||
double_hit: None,
|
|
||||||
press_origin: None,
|
press_origin: None,
|
||||||
mode,
|
mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selected_text(&self) -> Option<String> {
|
pub fn selected_text(&self) -> Option<String> {
|
||||||
let sel = self.selection?;
|
self.view.selection.selected_text(self.view.buf.text())
|
||||||
if sel.is_collapsed() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(self.buf.text()[sel.text_range()].to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The field's content. Byte-indexed, like everything else here since
|
/// The field's content. Byte-indexed, like everything else here since
|
||||||
@@ -67,46 +61,19 @@ impl TextEdit {
|
|||||||
/// The selection as a byte range, collapsed to `caret..caret` when
|
/// The selection as a byte range, collapsed to `caret..caret` when
|
||||||
/// there is no span. `None` when the field is not focused.
|
/// there is no span. `None` when the field is not focused.
|
||||||
pub fn selection_range(&self) -> Option<std::ops::Range<usize>> {
|
pub fn selection_range(&self) -> Option<std::ops::Range<usize>> {
|
||||||
Some(self.selection?.text_range())
|
self.view.selection.range()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The caret's byte offset -- the focus end of the selection, which is
|
/// The caret's byte offset -- the focus end of the selection, which is
|
||||||
/// where typing lands regardless of which end of a span it is.
|
/// where typing lands regardless of which end of a span it is.
|
||||||
pub fn caret(&self) -> Option<usize> {
|
pub fn caret(&self) -> Option<usize> {
|
||||||
Some(self.selection?.focus().index())
|
self.view.selection.caret()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for TextEdit {
|
impl Widget for TextEdit {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
let base = painter.layer;
|
let used = self.view.draw_selectable(painter, true);
|
||||||
painter.child_layer();
|
|
||||||
let used = self.view.draw(painter);
|
|
||||||
painter.layer = base;
|
|
||||||
let region = self.region();
|
|
||||||
|
|
||||||
let Some(selection) = self.selection else {
|
|
||||||
painter.set_size(used);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let layout = self.view.buf.layout();
|
|
||||||
|
|
||||||
for (rect, _) in selection.geometry(layout) {
|
|
||||||
let size = vec2(rect.width() as f32, rect.height() as f32);
|
|
||||||
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
|
|
||||||
painter.primitive_within(
|
|
||||||
RectPrimitive::color(Color::SKY),
|
|
||||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let caret = selection.focus().geometry(layout, CARET_WIDTH);
|
|
||||||
let size = vec2(caret.width() as f32, caret.height() as f32);
|
|
||||||
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
|
||||||
painter.primitive_within(
|
|
||||||
RectPrimitive::color(Color::WHITE),
|
|
||||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
|
||||||
);
|
|
||||||
painter.set_size(used);
|
painter.set_size(used);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,28 +89,26 @@ impl Widget for TextEdit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CARET_WIDTH: f32 = 1.0;
|
|
||||||
|
|
||||||
pub struct TextEditCtx<'a> {
|
pub struct TextEditCtx<'a> {
|
||||||
pub text: &'a mut TextEdit,
|
pub text: &'a mut TextEdit,
|
||||||
pub data: &'a mut TextData,
|
pub data: &'a mut TextData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TextEditCtx<'a> {
|
impl<'a> TextEditCtx<'a> {
|
||||||
fn layout(&mut self) -> &Layout<UiColor> {
|
fn selection_ctx(&mut self) -> TextSelectionCtx<'_> {
|
||||||
let attrs = self.text.view.attrs.clone();
|
TextSelectionCtx {
|
||||||
let width = self.text.view.wrap_width();
|
view: &mut self.text.view,
|
||||||
let density = self.data.density;
|
data: self.data,
|
||||||
self.text.view.buf.shape(self.data, &attrs, width, density);
|
}
|
||||||
self.text.view.buf.layout()
|
}
|
||||||
|
|
||||||
|
fn layout(&mut self) -> &Layout<iris_core::PaintId> {
|
||||||
|
selection_layout(&mut self.text.view, self.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(target_os = "android", allow(dead_code))]
|
#[cfg_attr(target_os = "android", allow(dead_code))]
|
||||||
fn refresh(&mut self) {
|
fn refresh(&mut self) {
|
||||||
if let Some(sel) = self.text.selection {
|
self.selection_ctx().refresh();
|
||||||
let layout = self.layout();
|
|
||||||
self.text.selection = Some(sel.refresh(layout));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn take(&mut self) -> String {
|
pub fn take(&mut self) -> String {
|
||||||
@@ -156,18 +121,18 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
let text = self.string(text);
|
let text = self.string(text);
|
||||||
self.text.view.buf.set_text(text);
|
self.text.view.buf.set_text(text);
|
||||||
self.text.view.buf.changed = true;
|
self.text.view.buf.changed = true;
|
||||||
self.text.selection = None;
|
self.text.view.selection.deselect();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_with_spans(&mut self, text: &str, spans: Vec<SpanStyle>) {
|
pub fn set_with_spans(&mut self, text: &str, spans: Vec<SpanStyle>) {
|
||||||
let text = self.string(text);
|
let text = self.string(text);
|
||||||
self.text.view.buf.set_text(text);
|
self.text.view.buf.set_text(text);
|
||||||
self.text.view.buf.set_spans(spans);
|
self.text.view.buf.set_spans(spans);
|
||||||
self.text.selection = None;
|
self.text.view.selection.deselect();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn motion(&mut self, motion: Motion, select: bool) {
|
pub fn motion(&mut self, motion: Motion, select: bool) {
|
||||||
let Some(sel) = self.text.selection else {
|
let Some(sel) = self.text.view.selection.range else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let layout = self.layout();
|
let layout = self.layout();
|
||||||
@@ -184,7 +149,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
} else {
|
} else {
|
||||||
apply_motion(sel, layout, motion, select)
|
apply_motion(sel, layout, motion, select)
|
||||||
};
|
};
|
||||||
self.text.selection = Some(sel);
|
self.text.view.selection.range = Some(sel);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replace(&mut self, len: usize, text: &str) {
|
pub fn replace(&mut self, len: usize, text: &str) {
|
||||||
@@ -213,7 +178,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.clear_span();
|
self.clear_span();
|
||||||
let at = match self.text.selection {
|
let at = match self.text.view.selection.range {
|
||||||
Some(sel) => sel.focus().index(),
|
Some(sel) => sel.focus().index(),
|
||||||
// No caret means nowhere to put the text, so this drops the
|
// No caret means nowhere to put the text, so this drops the
|
||||||
// keystroke -- which is invisible, and was the whole of the
|
// keystroke -- which is invisible, and was the whole of the
|
||||||
@@ -238,7 +203,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_span(&mut self) -> bool {
|
pub fn clear_span(&mut self) -> bool {
|
||||||
let Some(sel) = self.text.selection else {
|
let Some(sel) = self.text.view.selection.range else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if sel.is_collapsed() {
|
if sel.is_collapsed() {
|
||||||
@@ -252,13 +217,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_caret(&mut self, index: usize) {
|
fn set_caret(&mut self, index: usize) {
|
||||||
let index = index.min(self.text.view.buf.text().len());
|
self.selection_ctx().set_caret(index);
|
||||||
let layout = self.layout();
|
|
||||||
self.text.selection = Some(Selection::from_byte_index(
|
|
||||||
layout,
|
|
||||||
index,
|
|
||||||
Affinity::default(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn newline(&mut self) {
|
pub fn newline(&mut self) {
|
||||||
@@ -271,7 +230,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
if self.clear_span() {
|
if self.clear_span() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(sel) = self.text.selection else {
|
let Some(sel) = self.text.view.selection.range else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let end = sel.focus().index();
|
let end = sel.focus().index();
|
||||||
@@ -291,7 +250,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
if self.clear_span() {
|
if self.clear_span() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(sel) = self.text.selection else {
|
let Some(sel) = self.text.view.selection.range else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let start = sel.focus().index();
|
let start = sel.focus().index();
|
||||||
@@ -350,67 +309,33 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
/// actually *on* something" checks its own ranges, which is what
|
/// actually *on* something" checks its own ranges, which is what
|
||||||
/// makes a tap in the padding hit no link.
|
/// makes a tap in the padding hit no link.
|
||||||
pub fn byte_at(&mut self, pos: Vec2, size: Vec2) -> usize {
|
pub fn byte_at(&mut self, pos: Vec2, size: Vec2) -> usize {
|
||||||
let pos = pos - self.text.region().top_left().to_abs(size);
|
self.selection_ctx().byte_at(pos, size)
|
||||||
let layout = self.layout();
|
|
||||||
Selection::from_point(layout, pos.x, pos.y).focus().index()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_all(&mut self) {
|
pub fn select_all(&mut self) {
|
||||||
let len = self.text.view.buf.text().len();
|
self.selection_ctx().select_all();
|
||||||
if len == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let layout = self.layout();
|
|
||||||
let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default());
|
|
||||||
let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default());
|
|
||||||
self.text.selection = Some(Selection::new(anchor, focus));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
||||||
let pos = pos - self.text.region().top_left().to_abs(size);
|
self.selection_ctx().select(pos, size, drag, recent);
|
||||||
let prev_sel = self.text.selection;
|
|
||||||
let prev_hit = self.text.double_hit;
|
|
||||||
|
|
||||||
let outcome = {
|
|
||||||
let layout = self.layout();
|
|
||||||
if drag {
|
|
||||||
prev_sel.map(|sel| (Some(sel.extend_to_point(layout, pos.x, pos.y)), prev_hit))
|
|
||||||
} else {
|
|
||||||
let hit = Selection::from_point(layout, pos.x, pos.y);
|
|
||||||
let index = hit.focus().index();
|
|
||||||
Some(if recent && prev_hit == Some(index) {
|
|
||||||
(Some(Selection::line_from_point(layout, pos.x, pos.y)), None)
|
|
||||||
} else if recent && prev_sel.map(|s| s.focus().index()) == Some(index) {
|
|
||||||
(
|
|
||||||
Some(Selection::word_from_point(layout, pos.x, pos.y)),
|
|
||||||
Some(index),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(Some(hit), None)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some((selection, double_hit)) = outcome {
|
|
||||||
self.text.selection = selection;
|
|
||||||
self.text.double_hit = double_hit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deselect(&mut self) {
|
pub fn deselect(&mut self) {
|
||||||
self.text.selection = None;
|
self.selection_ctx().deselect();
|
||||||
self.text.double_hit = None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
|
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
|
||||||
let old = (self.text.view.buf.text().to_string(), self.text.selection);
|
let old = (
|
||||||
|
self.text.view.buf.text().to_string(),
|
||||||
|
self.text.view.selection.range,
|
||||||
|
);
|
||||||
let mut undo = false;
|
let mut undo = false;
|
||||||
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
||||||
if undo {
|
if undo {
|
||||||
if let Some((old, selection)) = self.text.history.pop() {
|
if let Some((old, selection)) = self.text.history.pop() {
|
||||||
self.set(&old);
|
self.set(&old);
|
||||||
self.text.selection = selection;
|
self.text.view.selection.range = selection;
|
||||||
self.refresh();
|
self.refresh();
|
||||||
}
|
}
|
||||||
} else if self.text.view.buf.text() != old.0 {
|
} else if self.text.view.buf.text() != old.0 {
|
||||||
@@ -495,7 +420,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
|
|
||||||
fn apply_motion(
|
fn apply_motion(
|
||||||
sel: Selection,
|
sel: Selection,
|
||||||
layout: &Layout<UiColor>,
|
layout: &Layout<PaintId>,
|
||||||
motion: Motion,
|
motion: Motion,
|
||||||
extend: bool,
|
extend: bool,
|
||||||
) -> Selection {
|
) -> Selection {
|
||||||
@@ -512,15 +437,15 @@ fn apply_motion(
|
|||||||
}
|
}
|
||||||
|
|
||||||
trait RangeCursors {
|
trait RangeCursors {
|
||||||
fn start_cursor(&self, layout: &Layout<UiColor>) -> parley::Cursor;
|
fn start_cursor(&self, layout: &Layout<PaintId>) -> parley::Cursor;
|
||||||
fn end_cursor(&self, layout: &Layout<UiColor>) -> parley::Cursor;
|
fn end_cursor(&self, layout: &Layout<PaintId>) -> parley::Cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RangeCursors for std::ops::Range<usize> {
|
impl RangeCursors for std::ops::Range<usize> {
|
||||||
fn start_cursor(&self, layout: &Layout<UiColor>) -> parley::Cursor {
|
fn start_cursor(&self, layout: &Layout<PaintId>) -> parley::Cursor {
|
||||||
parley::Cursor::from_byte_index(layout, self.start, Affinity::default())
|
parley::Cursor::from_byte_index(layout, self.start, Affinity::default())
|
||||||
}
|
}
|
||||||
fn end_cursor(&self, layout: &Layout<UiColor>) -> parley::Cursor {
|
fn end_cursor(&self, layout: &Layout<PaintId>) -> parley::Cursor {
|
||||||
parley::Cursor::from_byte_index(layout, self.end, Affinity::default())
|
parley::Cursor::from_byte_index(layout, self.end, Affinity::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -605,7 +530,7 @@ mod tests {
|
|||||||
ctx(&mut t, &mut d).set_caret(1);
|
ctx(&mut t, &mut d).set_caret(1);
|
||||||
ctx(&mut t, &mut d).insert("b");
|
ctx(&mut t, &mut d).insert("b");
|
||||||
assert_eq!(content(&t), "abc");
|
assert_eq!(content(&t), "abc");
|
||||||
assert_eq!(t.selection.unwrap().focus().index(), 2);
|
assert_eq!(t.caret(), Some(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -655,14 +580,14 @@ mod tests {
|
|||||||
ctx(&mut t, &mut d).select_all();
|
ctx(&mut t, &mut d).select_all();
|
||||||
assert!(ctx(&mut t, &mut d).clear_span());
|
assert!(ctx(&mut t, &mut d).clear_span());
|
||||||
assert_eq!(content(&t), "");
|
assert_eq!(content(&t), "");
|
||||||
assert_eq!(t.selection.unwrap().focus().index(), 0);
|
assert_eq!(t.caret(), Some(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tapping_an_empty_field_places_a_caret_so_typing_lands() {
|
fn tapping_an_empty_field_places_a_caret_so_typing_lands() {
|
||||||
let (mut t, mut d) = edit("", EditMode::MultiLine);
|
let (mut t, mut d) = edit("", EditMode::MultiLine);
|
||||||
ctx(&mut t, &mut d).select(vec2(40.0, 20.0), vec2(1080.0, 2400.0), false, false);
|
ctx(&mut t, &mut d).select(vec2(40.0, 20.0), vec2(1080.0, 2400.0), false, false);
|
||||||
assert!(t.selection.is_some(), "a tap must leave a caret behind");
|
assert!(t.caret().is_some(), "a tap must leave a caret behind");
|
||||||
ctx(&mut t, &mut d).insert("hi");
|
ctx(&mut t, &mut d).insert("hi");
|
||||||
assert_eq!(content(&t), "hi");
|
assert_eq!(content(&t), "hi");
|
||||||
}
|
}
|
||||||
@@ -671,14 +596,14 @@ mod tests {
|
|||||||
fn tapping_past_the_end_of_the_text_clamps_to_the_end() {
|
fn tapping_past_the_end_of_the_text_clamps_to_the_end() {
|
||||||
let (mut t, mut d) = edit("abc", EditMode::MultiLine);
|
let (mut t, mut d) = edit("abc", EditMode::MultiLine);
|
||||||
ctx(&mut t, &mut d).select(vec2(9000.0, 9000.0), vec2(1080.0, 2400.0), false, false);
|
ctx(&mut t, &mut d).select(vec2(9000.0, 9000.0), vec2(1080.0, 2400.0), false, false);
|
||||||
assert_eq!(t.selection.unwrap().focus().index(), 3);
|
assert_eq!(t.caret(), Some(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dragging_without_a_previous_selection_selects_nothing() {
|
fn dragging_without_a_previous_selection_selects_nothing() {
|
||||||
let (mut t, mut d) = edit("abc", EditMode::MultiLine);
|
let (mut t, mut d) = edit("abc", EditMode::MultiLine);
|
||||||
ctx(&mut t, &mut d).select(vec2(10.0, 10.0), vec2(1080.0, 2400.0), true, false);
|
ctx(&mut t, &mut d).select(vec2(10.0, 10.0), vec2(1080.0, 2400.0), true, false);
|
||||||
assert!(t.selection.is_none());
|
assert!(t.selection_range().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -765,7 +690,7 @@ mod tests {
|
|||||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).set_caret(0);
|
ctx(&mut t, &mut d).set_caret(0);
|
||||||
ctx(&mut t, &mut d).motion(Motion::Right, false);
|
ctx(&mut t, &mut d).motion(Motion::Right, false);
|
||||||
assert_eq!(t.selection.unwrap().focus().index(), 1);
|
assert_eq!(t.caret(), Some(1));
|
||||||
ctx(&mut t, &mut d).motion(Motion::Right, true);
|
ctx(&mut t, &mut d).motion(Motion::Right, true);
|
||||||
assert_eq!(t.selected_text().as_deref(), Some("b"));
|
assert_eq!(t.selected_text().as_deref(), Some("b"));
|
||||||
}
|
}
|
||||||
@@ -775,11 +700,11 @@ mod tests {
|
|||||||
let (mut t, mut d) = edit("abcdef", EditMode::SingleLine);
|
let (mut t, mut d) = edit("abcdef", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).select_all();
|
ctx(&mut t, &mut d).select_all();
|
||||||
ctx(&mut t, &mut d).motion(Motion::Left, false);
|
ctx(&mut t, &mut d).motion(Motion::Left, false);
|
||||||
assert_eq!(t.selection.unwrap().focus().index(), 0);
|
assert_eq!(t.caret(), Some(0));
|
||||||
|
|
||||||
ctx(&mut t, &mut d).select_all();
|
ctx(&mut t, &mut d).select_all();
|
||||||
ctx(&mut t, &mut d).motion(Motion::Right, false);
|
ctx(&mut t, &mut d).motion(Motion::Right, false);
|
||||||
assert_eq!(t.selection.unwrap().focus().index(), 6);
|
assert_eq!(t.caret(), Some(6));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+146
-2
@@ -1,9 +1,11 @@
|
|||||||
mod build;
|
mod build;
|
||||||
mod edit;
|
mod edit;
|
||||||
|
mod selection;
|
||||||
|
|
||||||
pub use build::*;
|
pub use build::*;
|
||||||
pub use edit::*;
|
pub use edit::*;
|
||||||
use iris_core::util::MutDetect;
|
use iris_core::util::MutDetect;
|
||||||
|
pub use selection::*;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
@@ -19,6 +21,7 @@ pub struct TextView {
|
|||||||
tex: Option<RenderedText>,
|
tex: Option<RenderedText>,
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
pub hint: Option<StrongWidget>,
|
pub hint: Option<StrongWidget>,
|
||||||
|
selection: TextSelection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextView {
|
impl TextView {
|
||||||
@@ -37,6 +40,7 @@ impl TextView {
|
|||||||
tex: None,
|
tex: None,
|
||||||
width: None,
|
width: None,
|
||||||
hint,
|
hint,
|
||||||
|
selection: TextSelection::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +98,39 @@ impl TextView {
|
|||||||
Size::abs(tex.size)
|
Size::abs(tex.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn draw_selectable(&mut self, painter: &mut Painter, caret: bool) -> Size {
|
||||||
|
let base = painter.layer;
|
||||||
|
painter.child_layer();
|
||||||
|
let used = self.draw(painter);
|
||||||
|
painter.layer = base;
|
||||||
|
let region = self.region();
|
||||||
|
|
||||||
|
let Some(selection) = self.selection.range else {
|
||||||
|
return used;
|
||||||
|
};
|
||||||
|
let layout = self.buf.layout();
|
||||||
|
for (rect, _) in selection.geometry(layout) {
|
||||||
|
let size = vec2(rect.width() as f32, rect.height() as f32);
|
||||||
|
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
|
||||||
|
let paint = painter.paint(&PaintId::SKY);
|
||||||
|
painter.primitive_within(
|
||||||
|
RectPrimitive::color(paint),
|
||||||
|
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if caret {
|
||||||
|
let caret = selection.focus().geometry(layout, CARET_WIDTH);
|
||||||
|
let size = vec2(caret.width() as f32, caret.height() as f32);
|
||||||
|
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
||||||
|
let paint = painter.paint(&PaintId::WHITE);
|
||||||
|
painter.primitive_within(
|
||||||
|
RectPrimitive::color(paint),
|
||||||
|
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
used
|
||||||
|
}
|
||||||
|
|
||||||
pub fn content(&self) -> String {
|
pub fn content(&self) -> String {
|
||||||
self.buf.text().to_string()
|
self.buf.text().to_string()
|
||||||
}
|
}
|
||||||
@@ -111,14 +148,36 @@ impl Text {
|
|||||||
if self.content.changed {
|
if self.content.changed {
|
||||||
self.content.changed = false;
|
self.content.changed = false;
|
||||||
self.view.buf.set_text(self.content.as_str());
|
self.view.buf.set_text(self.content.as_str());
|
||||||
|
self.view.selection.deselect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn selected_text(&self) -> Option<String> {
|
||||||
|
self.view.selection.selected_text(self.view.buf.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selection_range(&self) -> Option<std::ops::Range<usize>> {
|
||||||
|
self.view.selection.range()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_with_spans(&mut self, content: impl Into<String>, spans: Vec<SpanStyle>) {
|
||||||
|
let content = content.into();
|
||||||
|
*self.content = content.clone();
|
||||||
|
self.content.changed = false;
|
||||||
|
self.view.buf.set_text(content);
|
||||||
|
self.view.buf.set_spans(spans);
|
||||||
|
self.view.selection.deselect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Text {
|
impl Widget for Text {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
self.update_buf();
|
self.update_buf();
|
||||||
let size = self.view.draw(painter);
|
let size = if self.view.selection.range.is_some() {
|
||||||
|
self.view.draw_selectable(painter, false)
|
||||||
|
} else {
|
||||||
|
self.view.draw(painter)
|
||||||
|
};
|
||||||
painter.set_size(size);
|
painter.set_size(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +186,8 @@ impl Widget for Text {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) const CARET_WIDTH: f32 = 1.0;
|
||||||
|
|
||||||
impl Deref for Text {
|
impl Deref for Text {
|
||||||
type Target = TextAttrs;
|
type Target = TextAttrs;
|
||||||
|
|
||||||
@@ -160,6 +221,89 @@ mod tests {
|
|||||||
use crate::layout_tests::TestRsc;
|
use crate::layout_tests::TestRsc;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
fn rendered_text(content: &str) -> (TestRsc, UiRenderState, WeakWidget<Text>, StrongWidget) {
|
||||||
|
let mut rsc = TestRsc {
|
||||||
|
ui: UiData::default(),
|
||||||
|
};
|
||||||
|
let text = wtext(content).add_strong(&mut rsc);
|
||||||
|
let id = text.weak();
|
||||||
|
let root = text.any();
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
render.resize((800.0, 600.0));
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
(rsc, render, id, root)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_text_and_edit_text_use_the_same_selection_engine() {
|
||||||
|
let (mut rsc, _render, text, _root) = rendered_text("hello there");
|
||||||
|
text.selection(&mut rsc).select_all();
|
||||||
|
|
||||||
|
let view = TextView::new(TextBuffer::new("hello there"), TextAttrs::default(), None);
|
||||||
|
let mut edit = TextEdit::new(view, EditMode::MultiLine);
|
||||||
|
let mut data = TextData::default();
|
||||||
|
TextEditCtx {
|
||||||
|
text: &mut edit,
|
||||||
|
data: &mut data,
|
||||||
|
}
|
||||||
|
.select_all();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rsc.ui.widgets[text].selection_range(),
|
||||||
|
edit.selection_range()
|
||||||
|
);
|
||||||
|
assert_eq!(rsc.ui.widgets[text].selected_text(), edit.selected_text());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changing_display_text_clears_its_now_stale_selection() {
|
||||||
|
let (mut rsc, mut render, text, root) = rendered_text("before");
|
||||||
|
text.selection(&mut rsc).select_all();
|
||||||
|
assert_eq!(
|
||||||
|
rsc.ui.widgets[text].selected_text().as_deref(),
|
||||||
|
Some("before")
|
||||||
|
);
|
||||||
|
|
||||||
|
*rsc.ui.widgets[text].content = "after".to_string();
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
|
||||||
|
assert_eq!(rsc.ui.widgets[text].selected_text(), None);
|
||||||
|
assert_eq!(rsc.ui.widgets[text].selection_range(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_text_draws_the_shared_highlight_without_an_editing_caret() {
|
||||||
|
let (mut rsc, mut render, text, root) = rendered_text("selected");
|
||||||
|
let plain = render.active[&text.id()].primitives.len();
|
||||||
|
text.selection(&mut rsc).select_all();
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
let selected = render.active[&text.id()].primitives.len();
|
||||||
|
|
||||||
|
let view = TextView::new(TextBuffer::new("selected"), TextAttrs::default(), None);
|
||||||
|
let edit = rsc
|
||||||
|
.ui
|
||||||
|
.widgets
|
||||||
|
.add_strong(TextEdit::new(view, EditMode::MultiLine));
|
||||||
|
let edit_id = edit.weak();
|
||||||
|
let edit_root = edit.any();
|
||||||
|
let mut edit_render = UiRenderState::new();
|
||||||
|
edit_render.resize((800.0, 600.0));
|
||||||
|
edit_render.update(&edit_root, &mut rsc);
|
||||||
|
edit_id.edit(&mut rsc).select_all();
|
||||||
|
edit_render.update(&edit_root, &mut rsc);
|
||||||
|
let editable = edit_render.active[&edit_id.id()].primitives.len();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
selected > plain,
|
||||||
|
"the selection added no highlight primitive"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
editable,
|
||||||
|
selected + 1,
|
||||||
|
"editable text should add exactly its caret to the shared highlight"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clearing_the_atlas_re_renders_cached_text_instead_of_reusing_it() {
|
fn clearing_the_atlas_re_renders_cached_text_instead_of_reusing_it() {
|
||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
@@ -167,7 +311,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let root = wtext("hello there")
|
let root = wtext("hello there")
|
||||||
.size(18)
|
.size(18)
|
||||||
.color(UiColor::WHITE)
|
.color(PaintId::WHITE)
|
||||||
.add_strong(&mut rsc)
|
.add_strong(&mut rsc)
|
||||||
.any();
|
.any();
|
||||||
let mut render = UiRenderState::new();
|
let mut render = UiRenderState::new();
|
||||||
|
|||||||
@@ -0,0 +1,756 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
use iris_core::{PaintId, TextData};
|
||||||
|
use parley::{Affinity, Layout, Selection as ParleySelection};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// The selection state shared by display text and editable text. Editing,
|
||||||
|
/// focus and IME state deliberately live in `TextEdit`; this owns only the
|
||||||
|
/// state whose meaning comes from a shaped text layout.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(super) struct TextSelection {
|
||||||
|
pub(super) range: Option<ParleySelection>,
|
||||||
|
double_hit: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextSelection {
|
||||||
|
pub(super) fn selected_text(&self, text: &str) -> Option<String> {
|
||||||
|
let selection = self.range?;
|
||||||
|
if selection.is_collapsed() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(text[selection.text_range()].to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn range(&self) -> Option<std::ops::Range<usize>> {
|
||||||
|
Some(self.range?.text_range())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn caret(&self) -> Option<usize> {
|
||||||
|
Some(self.range?.focus().index())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn deselect(&mut self) {
|
||||||
|
self.range = None;
|
||||||
|
self.double_hit = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selection operations that need both a text widget's shaped buffer and
|
||||||
|
/// iris's text resources. `TextEditCtx` delegates to this same context rather
|
||||||
|
/// than maintaining an editable-only copy of the geometry and hit testing.
|
||||||
|
pub struct TextSelectionCtx<'a> {
|
||||||
|
pub(super) view: &'a mut TextView,
|
||||||
|
pub(super) data: &'a mut TextData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextSelectionCtx<'_> {
|
||||||
|
pub(super) fn layout(&mut self) -> &Layout<PaintId> {
|
||||||
|
selection_layout(self.view, self.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn refresh(&mut self) {
|
||||||
|
if let Some(selection) = self.view.selection.range {
|
||||||
|
let layout = self.layout();
|
||||||
|
self.view.selection.range = Some(selection.refresh(layout));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The byte offset in the text nearest `pos`. Positions and `size` use
|
||||||
|
/// the same widget-local coordinates as a `CursorSense` event.
|
||||||
|
pub fn byte_at(&mut self, pos: Vec2, size: Vec2) -> usize {
|
||||||
|
let pos = pos - self.view.region().top_left().to_abs(size);
|
||||||
|
let layout = self.layout();
|
||||||
|
ParleySelection::from_point(layout, pos.x, pos.y)
|
||||||
|
.focus()
|
||||||
|
.index()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_all(&mut self) {
|
||||||
|
let len = self.view.buf.text().len();
|
||||||
|
if len == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let layout = self.layout();
|
||||||
|
let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default());
|
||||||
|
let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default());
|
||||||
|
self.view.selection.range = Some(ParleySelection::new(anchor, focus));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
||||||
|
let pos = pos - self.view.region().top_left().to_abs(size);
|
||||||
|
let previous = self.view.selection.range;
|
||||||
|
let previous_hit = self.view.selection.double_hit;
|
||||||
|
|
||||||
|
let outcome = {
|
||||||
|
let layout = self.layout();
|
||||||
|
if drag {
|
||||||
|
previous.map(|selection| {
|
||||||
|
(
|
||||||
|
Some(selection.extend_to_point(layout, pos.x, pos.y)),
|
||||||
|
previous_hit,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let hit = ParleySelection::from_point(layout, pos.x, pos.y);
|
||||||
|
let index = hit.focus().index();
|
||||||
|
Some(if recent && previous_hit == Some(index) {
|
||||||
|
(
|
||||||
|
Some(ParleySelection::line_from_point(layout, pos.x, pos.y)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
} else if recent
|
||||||
|
&& previous.map(|selection| selection.focus().index()) == Some(index)
|
||||||
|
{
|
||||||
|
(
|
||||||
|
Some(ParleySelection::word_from_point(layout, pos.x, pos.y)),
|
||||||
|
Some(index),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(Some(hit), None)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((range, double_hit)) = outcome {
|
||||||
|
self.view.selection.range = range;
|
||||||
|
self.view.selection.double_hit = double_hit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deselect(&mut self) {
|
||||||
|
self.view.selection.deselect();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_caret(&mut self, index: usize) {
|
||||||
|
let index = index.min(self.view.buf.text().len());
|
||||||
|
let layout = self.layout();
|
||||||
|
self.view.selection.range = Some(ParleySelection::from_byte_index(
|
||||||
|
layout,
|
||||||
|
index,
|
||||||
|
Affinity::default(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_between(&mut self, anchor: usize, focus: usize) {
|
||||||
|
let len = self.view.buf.text().len();
|
||||||
|
let layout = self.layout();
|
||||||
|
let anchor = parley::Cursor::from_byte_index(layout, anchor.min(len), Affinity::default());
|
||||||
|
let focus = parley::Cursor::from_byte_index(layout, focus.min(len), Affinity::default());
|
||||||
|
self.view.selection.range = Some(ParleySelection::new(anchor, focus));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn selection_layout<'a>(
|
||||||
|
view: &'a mut TextView,
|
||||||
|
data: &mut TextData,
|
||||||
|
) -> &'a Layout<PaintId> {
|
||||||
|
let attrs = view.attrs.clone();
|
||||||
|
let width = view.wrap_width();
|
||||||
|
let density = data.density;
|
||||||
|
view.buf.shape(data, &attrs, width, density);
|
||||||
|
view.buf.layout()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gives an ordinary `Text` handle access to the same selection operations as
|
||||||
|
/// `TextEditCtx`. Gesture policy is intentionally not part of this trait; a
|
||||||
|
/// selection controller and an editor's focus handler do different
|
||||||
|
/// things with the same mechanics.
|
||||||
|
pub trait TextSelectable {
|
||||||
|
fn selection<'a>(&self, ui: &'a mut impl UiRsc) -> TextSelectionCtx<'a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: IdLike<Widget = Text>> TextSelectable for I {
|
||||||
|
fn selection<'a>(&self, ui: &'a mut impl UiRsc) -> TextSelectionCtx<'a> {
|
||||||
|
let ui = ui.ui_mut();
|
||||||
|
TextSelectionCtx {
|
||||||
|
view: &mut ui.widgets.get_mut(self).unwrap().view,
|
||||||
|
data: &mut ui.text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selection across the ordinary `Text` descendants of the widget this
|
||||||
|
/// controller is attached to. The controller owns the cross-widget gesture
|
||||||
|
/// and command state; each text leaf owns only its local Parley selection.
|
||||||
|
pub struct SelectionController {
|
||||||
|
anchor: Option<(WidgetId, usize)>,
|
||||||
|
order: Vec<WidgetId>,
|
||||||
|
selected: Vec<WidgetId>,
|
||||||
|
gesture: DragGesture,
|
||||||
|
scroll: Option<WeakWidget<LazySpan>>,
|
||||||
|
separator: String,
|
||||||
|
last_input: Option<(Instant, CursorSense, SelectionInput)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum SelectionInput {
|
||||||
|
Tapped,
|
||||||
|
Handled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SelectionController {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectionController {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
anchor: None,
|
||||||
|
order: Vec::new(),
|
||||||
|
selected: Vec::new(),
|
||||||
|
gesture: DragGesture::new(),
|
||||||
|
scroll: None,
|
||||||
|
separator: String::new(),
|
||||||
|
last_input: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_scroll(mut self, scroll: WeakWidget<LazySpan>) -> Self {
|
||||||
|
self.scroll = Some(scroll);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn separator(mut self, separator: impl Into<String>) -> Self {
|
||||||
|
self.separator = separator.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_order(host: WidgetId, rsc: &impl UiRsc, render: &UiRenderState) -> Vec<WidgetId> {
|
||||||
|
fn visit(id: WidgetId, rsc: &impl UiRsc, render: &UiRenderState, out: &mut Vec<WidgetId>) {
|
||||||
|
if rsc
|
||||||
|
.widgets()
|
||||||
|
.get_dyn(id)
|
||||||
|
.is_some_and(|widget| widget.as_any().is::<Text>())
|
||||||
|
{
|
||||||
|
out.push(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// An editor owns its own focus, commands and selection gesture.
|
||||||
|
if rsc
|
||||||
|
.widgets()
|
||||||
|
.get_dyn(id)
|
||||||
|
.is_some_and(|widget| widget.as_any().is::<TextEdit>())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for child in render.ordered_children(id, rsc) {
|
||||||
|
visit(child, rsc, render, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
visit(host, rsc, render, &mut out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_text<T>(
|
||||||
|
rsc: &mut impl UiRsc,
|
||||||
|
id: WidgetId,
|
||||||
|
f: impl FnOnce(&mut TextSelectionCtx<'_>) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
|
let ui = rsc.ui_mut();
|
||||||
|
let text = ui
|
||||||
|
.widgets
|
||||||
|
.get_dyn_mut(id)?
|
||||||
|
.as_any_mut()
|
||||||
|
.downcast_mut::<Text>()?;
|
||||||
|
text.update_buf();
|
||||||
|
let mut ctx = TextSelectionCtx {
|
||||||
|
view: &mut text.view,
|
||||||
|
data: &mut ui.text,
|
||||||
|
};
|
||||||
|
Some(f(&mut ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn locate(
|
||||||
|
&self,
|
||||||
|
rsc: &impl UiRsc,
|
||||||
|
render: &UiRenderState,
|
||||||
|
pos: Vec2,
|
||||||
|
) -> Option<(WidgetId, Vec2, Vec2)> {
|
||||||
|
self.order.iter().find_map(|&id| {
|
||||||
|
let active = render.active.get(&id)?;
|
||||||
|
let region = render.window_region(&id, rsc)?;
|
||||||
|
(region.contains(pos) && render.mask_admits(active.mask, pos, rsc)).then(|| {
|
||||||
|
(
|
||||||
|
id,
|
||||||
|
pos - region.top_left,
|
||||||
|
region.bot_right - region.top_left,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deselect(&mut self, rsc: &mut impl UiRsc) {
|
||||||
|
let mut ids = std::mem::take(&mut self.selected);
|
||||||
|
if let Some((anchor, _)) = self.anchor.take()
|
||||||
|
&& !ids.contains(&anchor)
|
||||||
|
{
|
||||||
|
ids.push(anchor);
|
||||||
|
}
|
||||||
|
for id in ids {
|
||||||
|
Self::with_text(rsc, id, |text| text.deselect());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin(&mut self, rsc: &mut impl UiRsc, id: WidgetId, pos: Vec2, size: Vec2) {
|
||||||
|
self.deselect(rsc);
|
||||||
|
let byte = Self::with_text(rsc, id, |text| {
|
||||||
|
text.select(pos, size, false, false);
|
||||||
|
text.byte_at(pos, size)
|
||||||
|
});
|
||||||
|
self.anchor = byte.map(|byte| (id, byte));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extend(&mut self, rsc: &mut impl UiRsc, id: WidgetId, pos: Vec2, size: Vec2) {
|
||||||
|
let Some((anchor, anchor_byte)) = self.anchor else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(anchor_at) = self.order.iter().position(|&candidate| candidate == anchor) else {
|
||||||
|
self.deselect(rsc);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(focus_at) = self.order.iter().position(|&candidate| candidate == id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(focus_byte) = Self::with_text(rsc, id, |text| text.byte_at(pos, size)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (lo, hi) = if anchor_at <= focus_at {
|
||||||
|
(anchor_at, focus_at)
|
||||||
|
} else {
|
||||||
|
(focus_at, anchor_at)
|
||||||
|
};
|
||||||
|
|
||||||
|
let old = std::mem::take(&mut self.selected);
|
||||||
|
for old_id in old {
|
||||||
|
if !self.order[lo..=hi].contains(&old_id) {
|
||||||
|
Self::with_text(rsc, old_id, |text| text.deselect());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for &text_id in &self.order[lo..=hi] {
|
||||||
|
let forward = anchor_at <= focus_at;
|
||||||
|
Self::with_text(rsc, text_id, |text| {
|
||||||
|
let len = text.view.buf.text().len();
|
||||||
|
let (start, end) = if text_id == anchor && text_id == id {
|
||||||
|
(anchor_byte, focus_byte)
|
||||||
|
} else if text_id == anchor {
|
||||||
|
(anchor_byte, if forward { len } else { 0 })
|
||||||
|
} else if text_id == id {
|
||||||
|
(if forward { 0 } else { len }, focus_byte)
|
||||||
|
} else {
|
||||||
|
(0, len)
|
||||||
|
};
|
||||||
|
text.select_between(start, end);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.selected = self.order[lo..=hi].to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drag<Rsc: HasEvents>(
|
||||||
|
&mut self,
|
||||||
|
id: ControllerId,
|
||||||
|
rsc: &mut Rsc,
|
||||||
|
input: &CursorData<'_>,
|
||||||
|
) -> SelectionInput {
|
||||||
|
// A leaf listener and the controller host may both cover one point on
|
||||||
|
// the same layer. They are two routes for one physical sample, not two
|
||||||
|
// gestures; the second route must observe the first route's decision.
|
||||||
|
if let Some((last, sense, outcome)) = self.last_input
|
||||||
|
&& last == input.cursor.time
|
||||||
|
&& sense == input.sense
|
||||||
|
{
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
self.order = Self::text_order(id.host(), rsc, input.render);
|
||||||
|
let hit = self.locate(rsc, input.render, input.cursor.pos);
|
||||||
|
let mut press = PressState::default();
|
||||||
|
if self.gesture.starts_press(input.sense) {
|
||||||
|
press.scrolling = self.scroll.is_some_and(|scroll| scroll(rsc).is_scrolling());
|
||||||
|
if let Some(scroll) = self.scroll {
|
||||||
|
scroll(rsc).cancel_fling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
press.already_selected = self.has_selection(rsc);
|
||||||
|
let outcome = self.gesture.handle(
|
||||||
|
input.pointer,
|
||||||
|
id.host(),
|
||||||
|
input.sense,
|
||||||
|
input.cursor.pos,
|
||||||
|
input.cursor.time,
|
||||||
|
press,
|
||||||
|
);
|
||||||
|
let input_result = match outcome {
|
||||||
|
GestureOutcome::Pan(delta) => {
|
||||||
|
if let Some(scroll) = self.scroll {
|
||||||
|
scroll(rsc).scroll(delta);
|
||||||
|
}
|
||||||
|
SelectionInput::Handled
|
||||||
|
}
|
||||||
|
GestureOutcome::SelectStart => {
|
||||||
|
if let Some((text, pos, size)) = hit {
|
||||||
|
self.begin(rsc, text, pos, size);
|
||||||
|
rsc.set_command_target(Some(id));
|
||||||
|
}
|
||||||
|
SelectionInput::Handled
|
||||||
|
}
|
||||||
|
GestureOutcome::SelectExtend => {
|
||||||
|
if let Some((text, pos, size)) = hit {
|
||||||
|
self.extend(rsc, text, pos, size);
|
||||||
|
}
|
||||||
|
SelectionInput::Handled
|
||||||
|
}
|
||||||
|
GestureOutcome::Released(Some(velocity)) => {
|
||||||
|
if let Some(scroll) = self.scroll
|
||||||
|
&& scroll(rsc).fling(velocity)
|
||||||
|
{
|
||||||
|
rsc.ui_mut().animate(scroll.id());
|
||||||
|
}
|
||||||
|
SelectionInput::Handled
|
||||||
|
}
|
||||||
|
GestureOutcome::Tapped => {
|
||||||
|
if self.anchor.is_some() || !self.selected.is_empty() {
|
||||||
|
self.deselect(rsc);
|
||||||
|
rsc.set_command_target(None);
|
||||||
|
SelectionInput::Handled
|
||||||
|
} else {
|
||||||
|
SelectionInput::Tapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GestureOutcome::Cancelled
|
||||||
|
| GestureOutcome::Undecided
|
||||||
|
| GestureOutcome::Released(None) => SelectionInput::Handled,
|
||||||
|
};
|
||||||
|
self.last_input = Some((input.cursor.time, input.sense, input_result));
|
||||||
|
input_result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_selection(&self, rsc: &impl UiRsc) -> bool {
|
||||||
|
self.selected.iter().any(|&id| {
|
||||||
|
rsc.widgets()
|
||||||
|
.get_dyn(id)
|
||||||
|
.and_then(|widget| widget.as_any().downcast_ref::<Text>())
|
||||||
|
.is_some_and(|text| text.selected_text().is_some())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_text(&self, rsc: &impl UiRsc) -> Option<String> {
|
||||||
|
let parts: Vec<String> = self
|
||||||
|
.selected
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&id| {
|
||||||
|
rsc.widgets()
|
||||||
|
.get_dyn(id)
|
||||||
|
.and_then(|widget| widget.as_any().downcast_ref::<Text>())
|
||||||
|
.and_then(Text::selected_text)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
(!parts.is_empty()).then(|| parts.join(&self.separator))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Rsc: HasEvents> Controller<Rsc> for SelectionController {
|
||||||
|
fn command(&mut self, command: Command, rsc: &mut Rsc) -> CommandResult {
|
||||||
|
match command {
|
||||||
|
Command::Copy => self
|
||||||
|
.selected_text(rsc)
|
||||||
|
.map(CommandResult::Copy)
|
||||||
|
.unwrap_or(CommandResult::Unused),
|
||||||
|
Command::SelectAll => {
|
||||||
|
let order = self.order.clone();
|
||||||
|
self.deselect(rsc);
|
||||||
|
for &id in &order {
|
||||||
|
Self::with_text(rsc, id, |text| text.select_all());
|
||||||
|
}
|
||||||
|
self.selected = order;
|
||||||
|
CommandResult::Used
|
||||||
|
}
|
||||||
|
Command::Escape => {
|
||||||
|
self.deselect(rsc);
|
||||||
|
CommandResult::Used
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod controller_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
struct TestRsc {
|
||||||
|
ui: UiData,
|
||||||
|
events: EventManager<TestRsc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiRsc for TestRsc {
|
||||||
|
fn ui(&self) -> &UiData {
|
||||||
|
&self.ui
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui_mut(&mut self) -> &mut UiData {
|
||||||
|
&mut self.ui
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_draw(&mut self, active: &ActiveData) {
|
||||||
|
self.events.draw(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_undraw(&mut self, active: &ActiveData) {
|
||||||
|
self.events.undraw(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_remove(&mut self, id: WidgetId) {
|
||||||
|
self.events.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasState for TestRsc {
|
||||||
|
type State = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasEvents for TestRsc {
|
||||||
|
fn events(&self) -> &EventManager<Self> {
|
||||||
|
&self.events
|
||||||
|
}
|
||||||
|
|
||||||
|
fn events_mut(&mut self) -> &mut EventManager<Self> {
|
||||||
|
&mut self.events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn two_texts(
|
||||||
|
dir: Dir,
|
||||||
|
) -> (
|
||||||
|
TestRsc,
|
||||||
|
UiRenderState,
|
||||||
|
WeakWidget<Span>,
|
||||||
|
WeakWidget<Text>,
|
||||||
|
WeakWidget<Text>,
|
||||||
|
StrongWidget,
|
||||||
|
) {
|
||||||
|
let mut rsc = TestRsc {
|
||||||
|
ui: UiData::default(),
|
||||||
|
events: EventManager::default(),
|
||||||
|
};
|
||||||
|
let first = wtext("first").add(&mut rsc);
|
||||||
|
let second = wtext("second").add(&mut rsc);
|
||||||
|
let host = (first, second)
|
||||||
|
.span(dir)
|
||||||
|
.controller(SelectionController::new().separator("|"))
|
||||||
|
.add(&mut rsc);
|
||||||
|
let root = host.upgrade(&mut rsc).any();
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
render.resize((400.0, 200.0));
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
(rsc, render, host, first, second, root)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_span_orders_selection_on_its_visual_axis() {
|
||||||
|
let (mut rsc, render, host, _first, _second, _root) = two_texts(Dir::LEFT);
|
||||||
|
let id = rsc
|
||||||
|
.events()
|
||||||
|
.controllers
|
||||||
|
.id::<SelectionController>(host.id())
|
||||||
|
.unwrap();
|
||||||
|
rsc.with_controller::<SelectionController, _>(id, |selection, rsc| {
|
||||||
|
selection.order = SelectionController::text_order(host.id(), rsc, &render);
|
||||||
|
});
|
||||||
|
rsc.set_command_target(Some(id));
|
||||||
|
|
||||||
|
assert_eq!(rsc.run_command(Command::SelectAll), CommandResult::Used);
|
||||||
|
assert_eq!(
|
||||||
|
rsc.run_command(Command::Copy),
|
||||||
|
CommandResult::Copy("second|first".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_widget_without_an_order_override_keeps_draw_order() {
|
||||||
|
let mut rsc = TestRsc {
|
||||||
|
ui: UiData::default(),
|
||||||
|
events: EventManager::default(),
|
||||||
|
};
|
||||||
|
let first = wtext("back").add(&mut rsc);
|
||||||
|
let second = wtext("front").add(&mut rsc);
|
||||||
|
let host = (first, second)
|
||||||
|
.stack()
|
||||||
|
.controller(SelectionController::new().separator("|"))
|
||||||
|
.add(&mut rsc);
|
||||||
|
let root = host.upgrade(&mut rsc).any();
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
render.resize((400.0, 200.0));
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
let id = rsc
|
||||||
|
.events()
|
||||||
|
.controllers
|
||||||
|
.id::<SelectionController>(host.id())
|
||||||
|
.unwrap();
|
||||||
|
rsc.with_controller::<SelectionController, _>(id, |selection, rsc| {
|
||||||
|
selection.order = SelectionController::text_order(host.id(), rsc, &render);
|
||||||
|
});
|
||||||
|
rsc.set_command_target(Some(id));
|
||||||
|
|
||||||
|
assert_eq!(rsc.run_command(Command::SelectAll), CommandResult::Used);
|
||||||
|
assert_eq!(
|
||||||
|
rsc.run_command(Command::Copy),
|
||||||
|
CommandResult::Copy("back|front".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nearest_controller_prefers_the_inner_scope() {
|
||||||
|
let mut rsc = TestRsc {
|
||||||
|
ui: UiData::default(),
|
||||||
|
events: EventManager::default(),
|
||||||
|
};
|
||||||
|
let leaf = wtext("leaf").add(&mut rsc);
|
||||||
|
let inner = (leaf,)
|
||||||
|
.span(Dir::DOWN)
|
||||||
|
.controller(SelectionController::new())
|
||||||
|
.add(&mut rsc);
|
||||||
|
let outer = (inner,)
|
||||||
|
.span(Dir::DOWN)
|
||||||
|
.controller(SelectionController::new())
|
||||||
|
.add(&mut rsc);
|
||||||
|
let root = outer.upgrade(&mut rsc).any();
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
render.resize((400.0, 200.0));
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
|
||||||
|
let found = rsc
|
||||||
|
.events()
|
||||||
|
.controllers
|
||||||
|
.nearest_id::<SelectionController>(leaf.id(), &render)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(found.host(), inner.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_target_outlives_pointer_release_and_copies_the_controller_selection() {
|
||||||
|
let (mut rsc, render, host, first, second, _root) = two_texts(Dir::DOWN);
|
||||||
|
let id = rsc
|
||||||
|
.events()
|
||||||
|
.controllers
|
||||||
|
.id::<SelectionController>(host.id())
|
||||||
|
.unwrap();
|
||||||
|
rsc.with_controller::<SelectionController, _>(id, |selection, rsc| {
|
||||||
|
selection.order = SelectionController::text_order(host.id(), rsc, &render);
|
||||||
|
let first_size = render.window_region(&first, rsc).unwrap().size();
|
||||||
|
let second_size = render.window_region(&second, rsc).unwrap().size();
|
||||||
|
selection.begin(rsc, first.id(), Vec2::ZERO, first_size);
|
||||||
|
selection.extend(rsc, second.id(), second_size, second_size);
|
||||||
|
});
|
||||||
|
rsc.set_command_target(Some(id));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rsc.run_command(Command::Copy),
|
||||||
|
CommandResult::Copy("first|second".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tapping_after_selection_deselects_and_releases_the_command_target() {
|
||||||
|
let (mut rsc, render, host, _first, _second, _root) = two_texts(Dir::DOWN);
|
||||||
|
let id = rsc
|
||||||
|
.events()
|
||||||
|
.controllers
|
||||||
|
.id::<SelectionController>(host.id())
|
||||||
|
.unwrap();
|
||||||
|
rsc.with_controller::<SelectionController, _>(id, |selection, rsc| {
|
||||||
|
selection.order = SelectionController::text_order(host.id(), rsc, &render);
|
||||||
|
let order = selection.order.clone();
|
||||||
|
for &text in &order {
|
||||||
|
SelectionController::with_text(rsc, text, |text| text.select_all());
|
||||||
|
}
|
||||||
|
selection.selected = order;
|
||||||
|
});
|
||||||
|
rsc.set_command_target(Some(id));
|
||||||
|
|
||||||
|
let pointer = PointerRequests::default();
|
||||||
|
let now = Instant::now();
|
||||||
|
rsc.with_controller::<SelectionController, _>(id, |selection, rsc| {
|
||||||
|
let press = CursorData {
|
||||||
|
pos: Vec2::ZERO,
|
||||||
|
size: Vec2::ZERO,
|
||||||
|
scroll_delta: Vec2::ZERO,
|
||||||
|
hover: Default::default(),
|
||||||
|
cursor: CursorState {
|
||||||
|
pos: Vec2::ZERO,
|
||||||
|
time: now,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
drag_axis: None,
|
||||||
|
captured: false,
|
||||||
|
sense: CursorSense::PressStart(CursorButton::Left),
|
||||||
|
render: &render,
|
||||||
|
pointer: &pointer,
|
||||||
|
};
|
||||||
|
selection.drag(id, rsc, &press);
|
||||||
|
let release = CursorData {
|
||||||
|
pos: Vec2::ZERO,
|
||||||
|
size: Vec2::ZERO,
|
||||||
|
scroll_delta: Vec2::ZERO,
|
||||||
|
hover: Default::default(),
|
||||||
|
cursor: CursorState {
|
||||||
|
pos: Vec2::ZERO,
|
||||||
|
time: now + std::time::Duration::from_millis(20),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
drag_axis: None,
|
||||||
|
captured: false,
|
||||||
|
sense: CursorSense::PressEnd(CursorButton::Left),
|
||||||
|
render: &render,
|
||||||
|
pointer: &pointer,
|
||||||
|
};
|
||||||
|
assert_eq!(selection.drag(id, rsc, &release), SelectionInput::Handled);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(rsc.events().controllers.command_target(), None);
|
||||||
|
assert_eq!(rsc.run_command(Command::Copy), CommandResult::Unused);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn removing_a_controller_host_clears_its_command_target() {
|
||||||
|
let (mut rsc, mut render, host, _first, _second, root) = two_texts(Dir::DOWN);
|
||||||
|
let id = rsc
|
||||||
|
.events()
|
||||||
|
.controllers
|
||||||
|
.id::<SelectionController>(host.id())
|
||||||
|
.unwrap();
|
||||||
|
rsc.set_command_target(Some(id));
|
||||||
|
|
||||||
|
drop(root);
|
||||||
|
render.update(None, &mut rsc);
|
||||||
|
rsc.free();
|
||||||
|
|
||||||
|
assert_eq!(rsc.events().controllers.command_target(), None);
|
||||||
|
assert_eq!(rsc.run_command(Command::Copy), CommandResult::Unused);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn removing_a_host_during_a_callback_does_not_restore_its_controller() {
|
||||||
|
let (mut rsc, _render, host, _first, _second, _root) = two_texts(Dir::DOWN);
|
||||||
|
let id = rsc
|
||||||
|
.events()
|
||||||
|
.controllers
|
||||||
|
.id::<SelectionController>(host.id())
|
||||||
|
.unwrap();
|
||||||
|
rsc.set_command_target(Some(id));
|
||||||
|
|
||||||
|
rsc.with_controller::<SelectionController, _>(id, |_selection, rsc| {
|
||||||
|
rsc.events_mut().controllers.remove(host.id());
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
rsc.events()
|
||||||
|
.controllers
|
||||||
|
.id::<SelectionController>(host.id())
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert_eq!(rsc.events().controllers.command_target(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
-41
@@ -9,22 +9,23 @@ pub fn build<Rsc: HasEvents>(rsc: &mut Rsc, ui_state: &mut impl HasRoot) -> Clie
|
|||||||
where
|
where
|
||||||
Rsc::State: FocusHost,
|
Rsc::State: FocusHost,
|
||||||
{
|
{
|
||||||
let rrect = rect(Color::WHITE).radius(20);
|
let rrect = rect(PaintId::WHITE).radius(20);
|
||||||
let pad_test = (
|
let pad_test = (
|
||||||
rrect.color(Color::BLUE),
|
rrect.clone().color(PaintId::BLUE),
|
||||||
(
|
(
|
||||||
rrect
|
rrect
|
||||||
.color(Color::RED)
|
.clone()
|
||||||
|
.color(PaintId::RED)
|
||||||
.sized((100, 100))
|
.sized((100, 100))
|
||||||
.center()
|
.center()
|
||||||
.width(rest(2)),
|
.width(rest(2)),
|
||||||
(
|
(
|
||||||
rrect.color(Color::ORANGE),
|
rrect.clone().color(PaintId::ORANGE),
|
||||||
rrect.color(Color::LIME).pad(10.0),
|
rrect.clone().color(PaintId::LIME).pad(10.0),
|
||||||
)
|
)
|
||||||
.span(Dir::RIGHT)
|
.span(Dir::RIGHT)
|
||||||
.width(rest(2)),
|
.width(rest(2)),
|
||||||
rrect.color(Color::YELLOW),
|
rrect.clone().color(PaintId::YELLOW),
|
||||||
)
|
)
|
||||||
.span(Dir::RIGHT)
|
.span(Dir::RIGHT)
|
||||||
.pad(10)
|
.pad(10)
|
||||||
@@ -34,19 +35,19 @@ where
|
|||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let span_test = (
|
let span_test = (
|
||||||
rrect.color(Color::GREEN).width(100),
|
rrect.clone().color(PaintId::GREEN).width(100),
|
||||||
rrect.color(Color::ORANGE),
|
rrect.clone().color(PaintId::ORANGE),
|
||||||
rrect.color(Color::CYAN),
|
rrect.clone().color(PaintId::CYAN),
|
||||||
rrect.color(Color::BLUE).width(rel(0.5)),
|
rrect.clone().color(PaintId::BLUE).width(rel(0.5)),
|
||||||
rrect.color(Color::MAGENTA).width(100),
|
rrect.clone().color(PaintId::MAGENTA).width(100),
|
||||||
rrect.color(Color::RED).width(100),
|
rrect.color(PaintId::RED).width(100),
|
||||||
)
|
)
|
||||||
.span(Dir::LEFT)
|
.span(Dir::LEFT)
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let span_add = Span::empty(Dir::RIGHT).add(rsc);
|
let span_add = Span::empty(Dir::RIGHT).add(rsc);
|
||||||
|
|
||||||
let add_button = rect(Color::LIME)
|
let add_button = rect(PaintId::LIME)
|
||||||
.radius(30)
|
.radius(30)
|
||||||
.on(CursorSense::click(), move |_, rsc| {
|
.on(CursorSense::click(), move |_, rsc| {
|
||||||
let child = image(include_bytes!("../assets/sungals.png"))
|
let child = image(include_bytes!("../assets/sungals.png"))
|
||||||
@@ -57,7 +58,7 @@ where
|
|||||||
.sized((150, 150))
|
.sized((150, 150))
|
||||||
.align(Align::BOT_RIGHT);
|
.align(Align::BOT_RIGHT);
|
||||||
|
|
||||||
let del_button = rect(Color::RED)
|
let del_button = rect(PaintId::RED)
|
||||||
.radius(30)
|
.radius(30)
|
||||||
.on(CursorSense::click(), move |_, rsc| {
|
.on(CursorSense::click(), move |_, rsc| {
|
||||||
span_add(rsc).pop();
|
span_add(rsc).pop();
|
||||||
@@ -79,9 +80,9 @@ where
|
|||||||
btext("'").family(Family::Monospace).align(Align::TOP),
|
btext("'").family(Family::Monospace).align(Align::TOP),
|
||||||
btext("'").family(Family::Monospace),
|
btext("'").family(Family::Monospace),
|
||||||
btext(":gamer mode").family(Family::Monospace),
|
btext(":gamer mode").family(Family::Monospace),
|
||||||
rect(Color::CYAN).sized((10, 10)).center(),
|
rect(PaintId::CYAN).sized((10, 10)).center(),
|
||||||
rect(Color::RED).sized((100, 100)).center(),
|
rect(PaintId::RED).sized((100, 100)).center(),
|
||||||
rect(Color::PURPLE).sized((50, 50)).align(Align::TOP),
|
rect(PaintId::PURPLE).sized((50, 50)).align(Align::TOP),
|
||||||
)
|
)
|
||||||
.span(Dir::RIGHT)
|
.span(Dir::RIGHT)
|
||||||
.center(),
|
.center(),
|
||||||
@@ -94,13 +95,13 @@ where
|
|||||||
let msg_area = texts
|
let msg_area = texts
|
||||||
.scrollable(Axis::Y, Pin::Start)
|
.scrollable(Axis::Y, Pin::Start)
|
||||||
.masked()
|
.masked()
|
||||||
.background(rect(Color::SKY));
|
.background(rect(PaintId::SKY));
|
||||||
let add_text = wtext("add")
|
let add_text = wtext("add")
|
||||||
.editable(EditMode::MultiLine)
|
.editable(EditMode::MultiLine)
|
||||||
.text_align(Align::LEFT)
|
.text_align(Align::LEFT)
|
||||||
.size(30)
|
.size(30)
|
||||||
.attr::<Selectable>(())
|
.attr::<Selectable>(())
|
||||||
.on(Submit, move |ctx, rsc| {
|
.on(Submit, move |ctx, rsc: &mut Rsc| {
|
||||||
let w = ctx.widget;
|
let w = ctx.widget;
|
||||||
let content = w.edit(rsc).take();
|
let content = w.edit(rsc).take();
|
||||||
let text = wtext(content)
|
let text = wtext(content)
|
||||||
@@ -109,9 +110,11 @@ where
|
|||||||
.text_align(Align::LEFT)
|
.text_align(Align::LEFT)
|
||||||
.wrap(true)
|
.wrap(true)
|
||||||
.attr::<Selectable>(());
|
.attr::<Selectable>(());
|
||||||
let msg_box = text
|
let fill = rsc
|
||||||
.background(rect(Color::WHITE.darker(0.5)))
|
.ui_mut()
|
||||||
.add_strong(rsc);
|
.paints
|
||||||
|
.add(Srgba8::WHITE.to_linear().darker(0.5));
|
||||||
|
let msg_box = text.background(rect(fill)).add_strong(rsc);
|
||||||
texts(rsc).push(msg_box);
|
texts(rsc).push(msg_box);
|
||||||
})
|
})
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
@@ -119,10 +122,14 @@ where
|
|||||||
let text_edit_scroll = (
|
let text_edit_scroll = (
|
||||||
msg_area.height(rest(1)),
|
msg_area.height(rest(1)),
|
||||||
(
|
(
|
||||||
Rect::new(Color::WHITE.darker(0.9)),
|
Rect::new(
|
||||||
|
rsc.ui_mut()
|
||||||
|
.paints
|
||||||
|
.add(Srgba8::WHITE.to_linear().darker(0.9)),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
add_text.width(rest(1)),
|
add_text.width(rest(1)),
|
||||||
Rect::new(Color::GREEN)
|
Rect::new(PaintId::GREEN)
|
||||||
.on(CursorSense::click(), move |ctx, rsc: &mut Rsc| {
|
.on(CursorSense::click(), move |ctx, rsc: &mut Rsc| {
|
||||||
rsc.run_event::<Submit>(add_text, (), ctx.state);
|
rsc.run_event::<Submit>(add_text, (), ctx.state);
|
||||||
})
|
})
|
||||||
@@ -142,7 +149,9 @@ where
|
|||||||
let main = WidgetPtr::new().add(rsc);
|
let main = WidgetPtr::new().add(rsc);
|
||||||
|
|
||||||
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
||||||
let mut switch_button = |color, to: WeakWidget, label| {
|
let mut switch_button = |solid: Srgba8, to: WeakWidget, label| {
|
||||||
|
let value = solid.to_linear();
|
||||||
|
let paint = rsc.ui_mut().paints.add(value);
|
||||||
let to = to.upgrade(rsc);
|
let to = to.upgrade(rsc);
|
||||||
let vec = &mut vals.borrow_mut().1;
|
let vec = &mut vals.borrow_mut().1;
|
||||||
let i = vec.len();
|
let i = vec.len();
|
||||||
@@ -153,38 +162,37 @@ where
|
|||||||
vec.push(Some(to));
|
vec.push(Some(to));
|
||||||
}
|
}
|
||||||
let vals = vals.clone();
|
let vals = vals.clone();
|
||||||
let rect = rect(color)
|
let pressed = paint.clone();
|
||||||
.on(CursorSense::click(), move |ctx, rsc| {
|
let hovered = paint.clone();
|
||||||
|
let normal = paint.clone();
|
||||||
|
let rect = rect(paint)
|
||||||
|
.on(CursorSense::click(), move |_ctx, rsc: &mut Rsc| {
|
||||||
let (prev, vec) = &mut *vals.borrow_mut();
|
let (prev, vec) = &mut *vals.borrow_mut();
|
||||||
if let Some(h) = vec[i].take() {
|
if let Some(h) = vec[i].take() {
|
||||||
vec[*prev] = main(rsc).replace(h);
|
vec[*prev] = main(rsc).replace(h);
|
||||||
*prev = i;
|
*prev = i;
|
||||||
}
|
}
|
||||||
ctx.widget(rsc).color = color.darker(0.3);
|
rsc.ui_mut().paints.set(&pressed, value.darker(0.3));
|
||||||
})
|
})
|
||||||
.on(
|
.on(
|
||||||
CursorSense::HoverStart | CursorSense::unclick(),
|
CursorSense::HoverStart | CursorSense::unclick(),
|
||||||
move |ctx, rsc| {
|
move |_ctx, rsc: &mut Rsc| {
|
||||||
ctx.widget(rsc).color = color.brighter(0.2);
|
rsc.ui_mut().paints.set(&hovered, value.brighter(0.2));
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.on(CursorSense::HoverEnd, move |ctx, rsc| {
|
.on(CursorSense::HoverEnd, move |_ctx, rsc: &mut Rsc| {
|
||||||
ctx.widget(rsc).color = color;
|
rsc.ui_mut().paints.set(&normal, value);
|
||||||
})
|
})
|
||||||
.label(label);
|
.label(label);
|
||||||
(rect, wtext(label).size(30).text_align(Align::CENTER)).stack()
|
(rect, wtext(label).size(30).text_align(Align::CENTER)).stack()
|
||||||
};
|
};
|
||||||
|
|
||||||
let tabs = (
|
let tabs = (
|
||||||
switch_button(Color::RED, pad_test, "pad"),
|
switch_button(Srgba8::RED, pad_test, "pad"),
|
||||||
switch_button(Color::GREEN, span_test, "span"),
|
switch_button(Srgba8::GREEN, span_test, "span"),
|
||||||
switch_button(Color::BLUE, span_add_test, "image span"),
|
switch_button(Srgba8::BLUE, span_add_test, "image span"),
|
||||||
switch_button(Color::MAGENTA, text_test, "text layout"),
|
switch_button(Srgba8::MAGENTA, text_test, "text layout"),
|
||||||
switch_button(
|
switch_button(Srgba8::YELLOW, text_edit_scroll, "text edit scroll"),
|
||||||
Color::YELLOW.mul_rgb(0.5),
|
|
||||||
text_edit_scroll,
|
|
||||||
"text edit scroll",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.span(Dir::RIGHT);
|
.span(Dir::RIGHT);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
|
use iris::{harness::Harness, prelude::*};
|
||||||
|
use pollster::FutureExt;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use wgpu::TextureFormat;
|
||||||
|
|
||||||
|
const SIZE: Vec2 = Vec2::new(2.0, 1.0);
|
||||||
|
const SOLID: Srgba8 = Srgba8::rgb(17, 127, 231);
|
||||||
|
const CHANGED_SOLID: Srgba8 = Srgba8::rgb(243, 139, 168);
|
||||||
|
const IMAGE: Srgba8 = Srgba8::rgb(205, 214, 244);
|
||||||
|
|
||||||
|
/// Covers the whole colour path rather than a conversion helper: an sRGB
|
||||||
|
/// literal enters the linear paint buffer, an sRGB image is sampled as
|
||||||
|
/// linear, the Iris shader returns both, and the sRGB attachment encodes the
|
||||||
|
/// stored bytes. Replacing only the paint-table entry also proves that a
|
||||||
|
/// theme change reaches an already-retained primitive.
|
||||||
|
#[test]
|
||||||
|
fn solid_paints_and_images_round_trip_through_an_srgb_target() {
|
||||||
|
let gpu = Gpu::open();
|
||||||
|
let mut harness = Harness::new(SIZE, 1.0);
|
||||||
|
let solid = harness.rsc.ui.paints.add(SOLID);
|
||||||
|
let bitmap = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
image::Rgba([IMAGE.r, IMAGE.g, IMAGE.b, IMAGE.a]),
|
||||||
|
));
|
||||||
|
let bitmap = image::<iris::harness::HarnessRsc>(bitmap)(&mut harness.rsc);
|
||||||
|
let root = (rect(solid.clone()).sized((1, 1)), bitmap)
|
||||||
|
.span(Dir::RIGHT)
|
||||||
|
.add_strong(&mut harness.rsc)
|
||||||
|
.any();
|
||||||
|
harness.state.set_root(root);
|
||||||
|
harness.frame(0);
|
||||||
|
|
||||||
|
let mut renderer =
|
||||||
|
UiRenderNode::new(&gpu.device, &gpu.queue, TextureFormat::Rgba8UnormSrgb, SIZE)
|
||||||
|
.expect("the Iris pipeline should accept an sRGB render target");
|
||||||
|
let first = render(&gpu, &mut renderer, &mut harness);
|
||||||
|
assert_pixel(first[0], SOLID, "linear paint buffer -> sRGB attachment");
|
||||||
|
assert_pixel(first[1], IMAGE, "sRGB texture -> shader -> sRGB attachment");
|
||||||
|
|
||||||
|
harness.rsc.ui.paints.set(&solid, CHANGED_SOLID);
|
||||||
|
let changed = render(&gpu, &mut renderer, &mut harness);
|
||||||
|
assert_pixel(
|
||||||
|
changed[0],
|
||||||
|
CHANGED_SOLID,
|
||||||
|
"updated paint table -> retained primitive",
|
||||||
|
);
|
||||||
|
assert_pixel(changed[1], IMAGE, "unchanged image after paint update");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(gpu: &Gpu, renderer: &mut UiRenderNode, harness: &mut Harness) -> [[u8; 4]; 2] {
|
||||||
|
renderer.update(
|
||||||
|
&gpu.device,
|
||||||
|
&gpu.queue,
|
||||||
|
&mut harness.rsc.ui,
|
||||||
|
&mut harness.render,
|
||||||
|
);
|
||||||
|
let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("Iris colour-space target"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: 2,
|
||||||
|
height: 1,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let view = texture.create_view(&Default::default());
|
||||||
|
let readback = gpu.device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("Iris colour-space readback"),
|
||||||
|
size: 256,
|
||||||
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
let mut encoder = gpu.device.create_command_encoder(&Default::default());
|
||||||
|
{
|
||||||
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("Iris colour-space pass"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
depth_slice: None,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(LinearRgba::BLACK.to_wgpu()),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
timestamp_writes: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
multiview_mask: None,
|
||||||
|
});
|
||||||
|
renderer.draw(&mut pass);
|
||||||
|
}
|
||||||
|
encoder.copy_texture_to_buffer(
|
||||||
|
texture.as_image_copy(),
|
||||||
|
wgpu::TexelCopyBufferInfo {
|
||||||
|
buffer: &readback,
|
||||||
|
layout: wgpu::TexelCopyBufferLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(256),
|
||||||
|
rows_per_image: Some(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wgpu::Extent3d {
|
||||||
|
width: 2,
|
||||||
|
height: 1,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
gpu.queue.submit([encoder.finish()]);
|
||||||
|
|
||||||
|
let slice = readback.slice(..);
|
||||||
|
slice.map_async(wgpu::MapMode::Read, |result| {
|
||||||
|
result.expect("mapping the colour-space readback")
|
||||||
|
});
|
||||||
|
gpu.device
|
||||||
|
.poll(wgpu::PollType::wait_indefinitely())
|
||||||
|
.expect("waiting for the colour-space readback");
|
||||||
|
let mapped = slice
|
||||||
|
.get_mapped_range()
|
||||||
|
.expect("reading the mapped colour-space buffer");
|
||||||
|
let pixels = [
|
||||||
|
mapped[0..4].try_into().unwrap(),
|
||||||
|
mapped[4..8].try_into().unwrap(),
|
||||||
|
];
|
||||||
|
drop(mapped);
|
||||||
|
readback.unmap();
|
||||||
|
pixels
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_pixel(got: [u8; 4], want: Srgba8, path: &str) {
|
||||||
|
let want = [want.r, want.g, want.b, want.a];
|
||||||
|
assert!(
|
||||||
|
got.into_iter()
|
||||||
|
.zip(want)
|
||||||
|
.all(|(got, want)| got.abs_diff(want) <= 1),
|
||||||
|
"{path}: stored {got:?}, expected {want:?} (within one code value)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vulkan_instance() -> &'static wgpu::Instance {
|
||||||
|
static INSTANCE: OnceLock<wgpu::Instance> = OnceLock::new();
|
||||||
|
INSTANCE.get_or_init(wgpu::Instance::default)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Gpu {
|
||||||
|
device: wgpu::Device,
|
||||||
|
queue: wgpu::Queue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gpu {
|
||||||
|
fn open() -> Self {
|
||||||
|
let adapter = vulkan_instance()
|
||||||
|
.request_adapter(&wgpu::RequestAdapterOptions::default())
|
||||||
|
.block_on()
|
||||||
|
.expect("no wgpu adapter, so Iris's colour-space pipeline went unchecked");
|
||||||
|
let info = adapter.get_info();
|
||||||
|
eprintln!(
|
||||||
|
"color_space: {} ({:?}, {})",
|
||||||
|
info.name, info.backend, info.driver
|
||||||
|
);
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.request_device(&wgpu::DeviceDescriptor {
|
||||||
|
required_limits: iris_core::device_limits(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.block_on()
|
||||||
|
.expect("could not get a device from the adapter");
|
||||||
|
Self { device, queue }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user