kivy/kivy — pull request #2048 by matham (2014-04-03 23:44:31+00:00)
Title: Reworks core.text layout and rendering and adds shorten options

This is a fairly large pr. I know @akshayaurora you hate large pr's but they all really are build on each other and I didn't want to submit anything until I fully tested every little feature. So here's a roadmap to what I did.

Changes
========

* Currently in master I had already updated for `core.text.label` a method to layout text which fixed some bug e.g. words were not wrapping properly across multiple lines etc. I generalized this method and moved it to cython as a single function that can be called to layout text (there's a commit at the start where it still in python). Moving it to cython increased speed by 2x.

* I introduced a `get_cached_extents` because when profiling `get_extents` had a big footprint. That improved speed by more than 2x. Together for label the laying out stage (`render(False)`) was improved by at least 4x in my test. The actual rendering (`render(True)`) didn't change much.

* I added `kivy/tests/visual_test_label.py` which allows you to performance test and basically test and target each label option. That's where I got my numbers. When constraining width to 200 and leaving height unconstrained I got the 4x improvement. I suggest you copy this test file outside kivy and use to comapre master with the pr.

* I changed both markup and to simply use the new cython `layout_text` function thereby eliminating all the bugs from both, including for markup where __kerning__ was ignored, __padding__ was ignored, long words didn't wrap properly across lines and others. I simply removed the `_pre_render_label` method because it is all done in `layout_text`. With a similar test as above using marked up text (`random_size` in `visual_test_label.py`) I got a 10x improvement both for the layout and rendering stage.

* I added new options to `shorten` (`shorten_from`, and `split_str`) and fully implemented shorten both in label and in markup label.

Test
------
Before anything, I suggest you play around with the `visual_test_label.py` and try to find bugs (you shouldn't be able to :)), do performance testing, and see what changed etc or if something doesn't look right. I used that fairly thoroughly and tried to make sure that every option combo worked. Also, markup should now behave almost identically to label except for the additional features.

Rationale
========

Here's the reason for making a layout function and moving it to cython. After fixing many issues in label I saw I had to now do the same thing for markup label :(. TextInput also has its own line wrapping bugs.

So instead, I figured it was smarter to create a single function which takes text, splits them into lines according to the options and returns a list of lines. I initially did it in python, but it really doesn't lend itself to it. In cython I could use inline functions to really clarify the code as much as possible while not losing any performance. Also, cython could really optimize these code forks allowing different options while not losing much performance. Truly, the function probably makes more sense as a label class method, but I didn't think it was a good idea to move label to cython.

Label, markup or any other module that needs to layout text across lines with possible constraints could just use this instead of rolling their own with probable bugs. I.e. move the complexity to a single place and make sure that works, instead of replicating stuff everywhere.

Towards that, instead of using tuples and indices to layout text into lines and then wondering what line[2][0][1] is, I defined `LayoutWord` and `LayoutLine` in cython so that we could now have a list of `LayoutLine` instance each describing a line, where each line is composed of text chunks stored in `LayoutWord` instances. Each have their options/w/h/is_last_line etc attributes. I could have used a dict, but this seems nicer. It definitely made it easier to avoid bugs.

In the future I see the possibility (maybe) that each `LayoutLine` instance caches/maps the positions of its words in some way so that TextInput would not have to do any get_extents calls to find cursor position because it'd be pre-calculated and it could get it from the line. That would almost certainly speed textinput up, especially since they are done from cython. But I don't really have the time to work on that now.

issues
======

Here's things that I'm aware of, that might need changing.

* `max_lines` for label cuts lines from top or bottom depending if valign is top or bottom. markup always cuts from bottom. I can markup to behave similarly if needed.
* when text_size[1] is not None, for markup, we have to layout everything when `valign` is `bottom` or `center` and only after do I cut it to size. For label, we layout only the minimum required. That means specifically for `valign` `center`, we find the newline that is about halfway through the text and layout text_size[1] / 2 downwards and upwards from there. The savings for large text is quite a bit, but it also means that center is not the same as for markup which finds the center after the text is all laid out.

In conclusion, this reworks the rendering and layout methods of label and markup label too rely on a cython layout function to do the layouting, so that label could jsut do the rendering. Shorten and justification is still done from from python, because it's mostly text manipulations which won't benefit much from cython. Anything I didn't mention, please ask me about.