avm1: implement scroll, maxscroll and bottomScroll for TextField (#4698)

* tests: add tests for scroll

* avm1: implement scroll, maxscroll, bottomScroll

* chore: fmt

* docs: note that scroll is 1-based

* fix: non-word wrapped text with manual breaks is scrollable

* chore: move magic number to const

* chore: avoid mut with extra if

* chore: moving clamping behaviour into core

* refactor: eagerly compute line data

* fix: make scroll work when text is aligned right

* chore: clippy

* docs: add more information about line_data

* tests: add more test cases for scroll
This commit is contained in:
Chris Midgley 2021-07-08 18:23:30 +01:00 committed by GitHub
parent d334e30259
commit ac0fc40345
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 236 additions and 2 deletions

View File

@ -78,6 +78,9 @@ const PROTO_DECLS: &[Declaration] = declare_properties! {
"password" => property(tf_getter!(password), tf_setter!(set_password));
"hscroll" => property(tf_getter!(hscroll), tf_setter!(set_hscroll));
"maxhscroll" => property(tf_getter!(maxhscroll));
"scroll" => property(tf_getter!(scroll), tf_setter!(set_scroll));
"maxscroll" => property(tf_getter!(maxscroll));
"bottomScroll" => property(tf_getter!(bottom_scroll));
};
/// Implements `TextField`
@ -617,3 +620,34 @@ pub fn maxhscroll<'gc>(
) -> Result<Value<'gc>, Error<'gc>> {
Ok(this.maxhscroll().into())
}
pub fn scroll<'gc>(
this: EditText<'gc>,
_activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Value<'gc>, Error<'gc>> {
Ok(this.scroll().into())
}
pub fn set_scroll<'gc>(
this: EditText<'gc>,
activation: &mut Activation<'_, 'gc, '_>,
value: Value<'gc>,
) -> Result<(), Error<'gc>> {
let input = value.coerce_to_f64(activation)?;
this.set_scroll(input, &mut activation.context);
Ok(())
}
pub fn maxscroll<'gc>(
this: EditText<'gc>,
_activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Value<'gc>, Error<'gc>> {
Ok(this.maxscroll().into())
}
pub fn bottom_scroll<'gc>(
this: EditText<'gc>,
_activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Value<'gc>, Error<'gc>> {
Ok(this.bottom_scroll().into())
}

View File

@ -153,8 +153,62 @@ pub struct EditTextData<'gc> {
/// Which rendering engine this text field will use.
render_settings: TextRenderSettings,
/// How many pixels right the text is offset by
/// How many pixels right the text is offset by. 0-based index.
hscroll: f64,
/// Information about the layout's current lines. Used by scroll properties.
line_data: Vec<LineData>,
/// How many lines down the text is offset by. 1-based index.
scroll: usize,
}
// TODO: would be nicer to compute (and return) this during layout, instead of afterwards
/// Compute line (index, offset, extent) from the layout data.
fn get_line_data(layout: &[LayoutBox]) -> Vec<LineData> {
// if there are no boxes, there are no lines
if layout.is_empty() {
return Vec::new();
}
let first_box = &layout[0];
let mut index = 1;
let mut offset = first_box.bounds().offset_y();
let mut extent = first_box.bounds().extent_y();
let mut line_data = Vec::new();
for layout_box in layout.get(1..).unwrap() {
let bounds = layout_box.bounds();
// if the top of the new box is lower than the bottom of the old box, it's a new line
if bounds.offset_y() > extent {
// save old line and reset
line_data.push(LineData {
index,
offset,
extent,
});
index += 1;
offset = bounds.offset_y();
extent = bounds.extent_y();
} else {
// otherwise we continue from the previous box
offset = offset.min(bounds.offset_y());
extent = extent.max(bounds.extent_y());
}
}
// save the final line
line_data.push(LineData {
index,
offset,
extent,
});
line_data
}
impl<'gc> EditText<'gc> {
@ -207,6 +261,7 @@ impl<'gc> EditText<'gc> {
swf_tag.is_word_wrap,
swf_tag.is_device_font,
);
let line_data = get_line_data(&layout);
let has_background = swf_tag.has_border;
let background_color = 0xFFFFFF; // Default is white
@ -285,6 +340,8 @@ impl<'gc> EditText<'gc> {
has_focus: false,
render_settings: Default::default(),
hscroll: 0.0,
line_data,
scroll: 1,
},
));
@ -770,10 +827,12 @@ impl<'gc> EditText<'gc> {
edit_text.is_device_font,
);
edit_text.line_data = get_line_data(&new_layout);
edit_text.layout = new_layout;
edit_text.intrinsic_bounds = intrinsic_bounds;
// reset scroll
edit_text.hscroll = 0.0;
edit_text.scroll = 1;
match autosize {
AutoSizeMode::None => {}
@ -850,6 +909,53 @@ impl<'gc> EditText<'gc> {
}
}
/// How many lines the text can be scrolled down
pub fn maxscroll(self) -> usize {
let edit_text = self.0.read();
let line_data = &edit_text.line_data;
if line_data.is_empty() {
return 1;
}
let target = line_data.last().unwrap().extent - edit_text.bounds.height();
// minimum line n such that n.offset > max.extent - bounds.height()
let max_line = line_data.iter().find(|&&l| target < l.offset);
if let Some(line) = max_line {
line.index
} else {
// I don't know how this could happen, so return the limit
line_data.last().unwrap().index
}
}
/// The lowest visible line of text
pub fn bottom_scroll(self) -> usize {
let edit_text = self.0.read();
let line_data = &edit_text.line_data;
if line_data.is_empty() {
return 1;
}
let scroll_offset = line_data
.get(edit_text.scroll - 1)
.map_or(Twips::ZERO, |l| l.offset);
let target = edit_text.bounds.height() + scroll_offset;
// Line before first line with extent greater than bounds.height() + line "scroll"'s offset
let too_far = line_data.iter().find(|&&l| l.extent > target);
if let Some(line) = too_far {
line.index - 1
} else {
// all lines are visible
line_data.last().unwrap().index
}
}
/// Render a layout box, plus its children.
fn render_layout_box(self, context: &mut RenderContext<'_, 'gc>, lbox: &LayoutBox<'gc>) {
let box_transform: Transform = lbox.bounds().origin().into();
@ -1131,6 +1237,23 @@ impl<'gc> EditText<'gc> {
self.0.write(context.gc_context).hscroll = hscroll;
}
pub fn scroll(self) -> usize {
self.0.read().scroll
}
pub fn set_scroll(self, scroll: f64, context: &mut UpdateContext<'_, 'gc, '_>) {
// derived experimentally. Not exact: overflows somewhere above 767100486418432.9
// Checked in SWF 6, AVM1. Same in AVM2.
const SCROLL_OVERFLOW_LIMIT: f64 = 767100486418433.0;
let scroll_lines = if scroll.is_nan() || scroll < 0.0 || scroll >= SCROLL_OVERFLOW_LIMIT {
1
} else {
scroll as usize
};
let clamped = scroll_lines.clamp(1, self.maxscroll());
self.0.write(context.gc_context).scroll = clamped;
}
pub fn screen_position_to_index(self, position: (Twips, Twips)) -> Option<usize> {
let text = self.0.read();
let position = self.global_to_local(position);
@ -1598,13 +1721,24 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> {
);
context.renderer.activate_mask();
let scroll_offset = if edit_text.scroll > 1 {
let line_data = &edit_text.line_data;
if let Some(line_data) = line_data.get(edit_text.scroll - 1) {
line_data.offset
} else {
Twips::ZERO
}
} else {
Twips::ZERO
};
// TODO: Where does this come from? How is this different than INTERNAL_PADDING? Does this apply to y as well?
// If this is actually right, offset the border in `redraw_border` instead of doing an extra push.
context.transform_stack.push(&Transform {
matrix: Matrix {
tx: Twips::from_pixels(Self::INTERNAL_PADDING)
- Twips::from_pixels(edit_text.hscroll),
ty: Twips::from_pixels(Self::INTERNAL_PADDING),
ty: Twips::from_pixels(Self::INTERNAL_PADDING) - scroll_offset,
..Default::default()
},
..Default::default()
@ -1785,6 +1919,17 @@ pub struct TextSelection {
to: usize,
}
/// Information about the start and end y-coordinates of a given line of text
#[derive(Copy, Clone, Debug, Collect)]
#[collect(require_static)]
pub struct LineData {
index: usize,
/// How many twips down the highest point of the line is
offset: Twips,
/// How many twips down the lowest point of the line is
extent: Twips,
}
impl TextSelection {
pub fn for_position(position: usize) -> Self {
Self {

View File

@ -282,6 +282,7 @@ swf_tests! {
#[ignore] (edittext_newlines, "avm1/edittext_newlines", 1),
(edittext_html_entity, "avm1/edittext_html_entity", 1),
(edittext_password, "avm1/edittext_password", 1),
(edittext_scroll, "avm1/edittext_scroll", 1),
#[ignore] (edittext_html_roundtrip, "avm1/edittext_html_roundtrip", 1),
(edittext_newline_stripping, "avm1/edittext_newline_stripping", 1),
(define_local, "avm1/define_local", 1),

View File

@ -0,0 +1,54 @@
// text field with width 100
// text: A test for scroll
// title_txt.maxscroll
1
// title_txt.bottomScroll
1
// title_txt.scroll = -10
1
// title_txt.scroll = 10
1
// text: A longer test for scroll. This entry has more than one line, so we must scroll to see more.
// title_txt.maxscroll
5
// title_txt.bottomScroll
2
// title_txt.scroll = -10
1
// title_txt.scroll = 2
2
// title_txt.bottomScroll
3
// title_txt.scroll = 100
5
// title_txt.bottomScroll
6
// title_txt.scroll = null
1
// title_txt.scroll = undefined
1
// title_txt.scroll = 1.5
1
// title_txt.scroll = "4"
4
// title_txt.scroll = 2147483647
5
// title_txt.scroll = 767100486418432
5
// title_txt.scroll = 767100486418432.9
5
// title_txt.scroll = 767100486418433
1
// right-aligned text box
// right_text.text = A longer test for scroll. This entry has more than one line, so we must scroll to see more.
// right_text.maxscroll
5
// right_text.bottomScroll
2
// text box with multiple font combinations
// funny_fonts.maxscroll
3
// funny_fonts.bottomScroll
2

Binary file not shown.

Binary file not shown.