function [nx, ny, textbounds, cache, wordbounds] = DrawFormattedText2(varargin) % [nx, ny, textbounds, cache, wordbounds] = DrawFormattedText2(tstring, key-value pairs) % or: % [nx, ny, textbounds, cache, wordbounds] = DrawFormattedText2(cache, key-value pairs) % % When called with a string, the following key-value pairs are understood: % win [, sx][, sy][, xalign][, yalign][, xlayout][, baseColor][, wrapat][, transform][, vSpacing][, righttoleft][, winRect][, resetStyle][, cacheOnly] % Those enclosed in square braces are optional. % % When called with a cache struct, the following optional key-value pair % arguments are accepted: % [, win][, sx][, sy][, xalign][, yalign][, transform][, winRect][, cacheOnly] % % example call: % DrawFormattedText2('test text', 'win',window_pointer,'baseColor',[255 0 0]) % % Draws a string of text 'tstring' into Psychtoolbox window 'win'. Allows % some formatting and precise positioning. Only works with the FTGL-based % text plugin (see help DrawTextPlugin), activate it with % Screen('Preference','TextRenderer', 1); % This function is _not_ made to be fast. Test carefully if you want it to % work within a screen refresh if you throw a lot of text at it. % % The text string 'tstring' may contain newline characters '\n'. % Whenever a newline character '\n' is encountered, a linefeed and % carriage return is performed, breaking the text string into lines. % % The text may also contain the formatting tags listed below. Each tag % changes the formatting of the text from that point onward. Tags do not % need to be closed. At the exit of the function, the state of the font % renderer (font, text size, etc) is reset to the state upon function entry % or the state stored in the cache if drawing from cache. % - : toggles italicization % - : toggles bolding % - : toggles underlining % - switches to a new color % - switches to a new font % - switches to a new font size % The , and tags toggle whether text is italicized, bolded or % underlined and remain active (possibly until the end of the input string) % until the same , or tag is encountered again. % The , and tags can be provided empty (i.e., without % argument), in which case they cause to revert back to the color, font or % size active before the previous switch. Multiple of these in a row go % back further in history (until start color, font, size is reached). % To escape a tag, prepend it with a slash, e.g., /. If you want a % slash right in front of a tag, escape it by making it a double /: % //. No other slashes should be escaped. % 's argument can have one of two formats, it can either be a % hexadecimal string (HEX), or a comma-separated floating point array % (FPN). If a HEX input is provided, it should encode colors using a 1, 2, % 6 or 8 hexadecimal digit string. If 1 or 2 digits are provided, this is % interpreted as a grayscale value. 6 hexadecimal digits are interpreted as % R, G and B values (2 digits each). 8 digits further includes an alpha % value. Using the HEX values, color values range from 0 to 255. Example: % changes the text color to red (ff corresponds to 255). The % FPN format consists of 1, 3 or 4 comma separated floating point color % values (luminance, RGB, or RGBA) that can take on any value, but will be % processed according to the current color settings for the window as % indicated by Screen('ColorRange'). Typically, the values provided will % range from 0.0--1.0 per component. Floating point values should include % the decimal point to ensure that single element FPN values are parsed % correctly. Examples: and . If a floating point % value is specified but Screen('ColorRange') returns 255, indicating 8bit % color values are used for the window, the floating point color values % provided are automatically scaled and rounded to the 0-255 range. If the % HEX format is used but Screen('ColorRange') returns 1.0, indicating % floating point color values are used for the window, the uint8 color % values are automatically converted to the 0.0-1.0 range. % A size/font command before a newline can change the height of the line on % which it occurs. So if you want to space two words 'test' and 'text' % vertically by white space equivalent to an 80pts line, use: % 'test\n\ntext'. There is an empty line between the two new % lines, and the size=80 says that this line has height of 80pts in the % selected font. % % 'sx' and 'sy' provide a location on the screen with respect to which the % textbox is positioned. 'sx' and 'sy' can be a pixel location provided as % a number. Alternatively, 'sx' can also be 'left' (default), 'center', or % 'right' signifying the left side, horizontal middle, or right side of the % window rect. 'sy' can also be 'top' (default), 'center', or 'bottom' % signifying the top, vertical middle, or bottom of the window rect. % % 'xalign' and 'yalign' are text strings indicating how the textbox should % be aligned to the screen location provided in 'sx' and 'sy'. For % 'xalign', 'left' (default), signifies that the left of the text's % bounding box is aligned to the screen location; 'center' that the % bounding box is centered on this location; and 'right' that the right of % the text's bounding box is aligned with this location. For 'yalign', % 'top' (default), signifies that the top of the text's bounding box is % aligned to the screen location; 'center' that the bounding box is % centered on this location; and 'bottom' that the bottom of the text's % bounding box is aligned with this location. % % 'xlayout' indicates how each line is positioned horizontally in the % bounding box. If 'left' (default), all lines are aligned to the left of % the text's bounding box; if 'center', all lines are centered in the % bounding box; and if 'right', all lines are aligned to the right of the % bounding box. Justification options are currently not supported. % % 'baseColor' is the color in which the text will be drawn (until changed % by a format call). This is also the color that will remain active % after invocation of this function. % % 'wrapat', if provided, will automatically break text strings longer than % 'wrapat' characters into newline separated strings of roughly 'wrapat' % characters. This is done by calling the WrapString function (See 'help % WrapString'). 'wrapat' mode may not work reliably with non-ASCII text % strings, e.g., UTF-8 encoded uint8 strings on all systems. It also does % not necessarily lead to lines of roughly equal length, unless using a % monospace font. % % 'transform' allows transforming the text to be drawn as a whole. The % 'transform' input is a cell array of key-value parameters, indicating % which transform to do in which order. The following transform are % supported: % 'translate', [dx dy]: translates text by dx horizontally and dy % vertically % 'flip', axis : mirrors text horizontally if axis is 1, vertically % if axis is 2, and along both axes if number is 3 % (which is equal to a 180 deg rotation) % 'scale', [sx sy] : scale text horizontally by sx and vertically by sy. % Set to 1 if you want no scaling. % 'rotate', angle : Rotate text by angle (degrees). Note that for the % PTB screen, a positive rotation is clockwise. % example {'translate',[100 0],'rotate',45} first translates text by 100 % pixels, then rotates it by 45 degree clockwise. Note that the order of % operations is important. The above is equal to % {'rotate',45,'translate',100*sqrt([2 2])/2}. Advanced note: for OpenGL % transform are applied in the reverse order from how they're specified. % That is not the case for this interface, transform are applied in the % order specified. % % The optional argument 'vSpacing' sets the spacing between the lines. % Default value is 1. % % The optional argument 'winRect' allows to specify a [left top right % bottom] rectangle, in which the text should be placed etc. By default, the % rectangle of the whole 'win'dow is used. % % 'resetStyle'. If true, we reset the base text style to normal before % interpreting formatting commands that are present in the input text % string. If not (false), active text style at function entry is taken into % account when processing style toggle tags % % 'cache'. Upon invocation of the function, it provides an optional output % 'cache'. Providing this output back to DrawFormattedText2 instead of the % 'tstring' input allows direct drawing of the exact same text without all % the preprocessing having to be done again, potentially saving significant % time (and simplifying the call syntax). In this mode, a subset of the % below arguments can be used to, e.g., draw to a different window or % reposition the text. This cache can be generated without any actual % drawing being done by setting the 'cacheOnly' argument to true. The cache % is an implementation detail and is subject to change at any time. % When drawing from cache, the text can be repositioned with the 'sx', % 'sy', 'xalign', 'yalign' and 'winRect' inputs described above. If only % 'sx' and 'sy' are provided, these are taken to be offsets to move the % bounding box of the cached text. 'sx' and 'sy' must be numerical in this % case and cannot be empty. If 'xalign' and/or 'yalign' are provided, % full-fledged parsing of 'sx', 'sy', 'xalign' and 'yalign' is done, same % as during a normal call to DrawFormattedText2. The bounding box is then % repositioned according to these for inputs. The winRect argument is only % used in this case. It is optional (defaulting to the whole windows), and % works as described above, specifying the rect to which 'xalign' and % 'yalign' apply. The input option 'transform' can furthermore be set, % which appends additional transformations to what is already in the cache. % % % Return variables: % % The function returns the new (nx, ny) position of the text drawing cursor % and the bounding rectangle 'textbounds' of the drawn string. (nx,ny) can % be used as new start position for connecting further text strings to the % bottom of the drawn text string. Calculation of textbounds is % approximative, so it may give wrong results with some text fonts and % styles on some operating systems, depending on the various settings. The % optional 'cache' output argument is discussed above. % % When rotating by angles that are not a multiple of 90 degrees, the % bounding box may not be accurate (too large). The returned bounding box % is the rect that tightly fits the rotated original bounding box, not the % box that tightly fits the rotated ink of the letters. % % The optional return argument 'wordbounds', if assigned in the calling % function, returns a n-by-4 matrix of per-word bounding boxes. Each row % defines a [left,top,right,bottom] rectangle with the bounding box of a % word in the text string, ie. row 1 = first word, row 2 = 2nd word, ... % white-space characters delimit single words, as do style changes and line- % feeds, and these delimiters are not taken into account for the bounding box, % ie. they don't get their own bounding boxes. The white-space separating % successive words is as defined by the function isspace(tstring), or by a % change of text style, color, formatting, etc. Use of 'wordbounds' may cause % a significant slow-down in text drawing, so only assign this return argument % if you actually need it. A current limitation is that returned bounding boxes % will be likely incorrect if you apply multiple transformations like 'scale' % 'rotate', 'translate' and 'flip' at once. A single transformation will work, % but multiple ones will cause misplaced per word bounding boxes. If you want % to get proper 'wordbounds' when drawing text from the 'cache' then you must % assign 'wordbounds' already in the DrawFormattedText2() invocation which % returns the 'cache', otherwise bounding boxes might be wrong. % % One difference in the return values from this function and % DrawFormattedText is that the new (nx, ny) position of the text drawing % cursor output is the baseline of the text. So to use (nx,ny) as the new % start position for connecting further text strings, you need to draw % these strings with yPositionIsBaseline==true. Another difference is that % the returned textbounds bounding box includes the height of an empty line % at the end if the input string ended with a carriage return. % DrawFormattedText only moved (nx,ny) but did not include the empty line % in the bounding box. The empty line is also taken into account when % centering text % % % Further Notes: % % Please note that while positioning and bounding boxes are pixel accurate % with the fonts tested during development, i cannot guarantee this is the % case with all fonts you throw at it. Also note that this function is not % made to be fast. It has a make draw from cache mode so that all % preprocessing is done only once and stored in a cache from which the text % can be drawn directly. Nonetheless, especially if you throw a lot of % formatting at it, this function may still not be fast in this mode. Use % with caution in timing critical paths (test and measure to know if its % fast enough). That said, with reasonable inputs it should manage to draw % text to screen well within the inter frame interval of most screens. % % The function employs clipping by default. Text lines that are detected as % laying completely outside the 'win'dow or optional 'winRect' will not be % drawn, but clipped away. This allows to draw multi-page text (multiple % screen heights) without too much loss of drawing speed. If you find the % clipping to interfere with text layout of exotic texts/fonts at exotic % sizes and formatting, you can define the global variable... % global ptb_drawformattedtext2_disableClipping; % ... and set it like this ... % ptb_drawformattedtext2_disableClipping = 1; % ... to disable the clipping. % % Regardless of the clipping setting, the optional 3rd return parameter % 'textbounds' always covers the complete text. % % See DrawFormattedText2Demo for a usage example. % TODO: % - justification. I have included an interface, but not implemented (or % documented it in the help above) yet. % - Fix per word bounding boxes (optionally grow them to be proper AOIs for % eye-tracking) for multiple concatenated 'Transform's. % History: % 2015--2017 Written (DCN). % 7-May-2017 Add support for 'wordbounds' - per word bounding boxes (MK). % 18-Apr-2021 Fix UTF-8 string handling WRT new Octave 6 regexp behavior (MR). global ptb_drawformattedtext2_disableClipping; global ptb_drawformattedtext2_padthresh; if isempty(ptb_drawformattedtext2_disableClipping) % Text clipping on by default: ptb_drawformattedtext2_disableClipping = 0; end % Boundinx boxes are always for the whole text, but text out of the screen % is culling/clipping. User can forcefully disable or enable this clipping. disableClip = (ptb_drawformattedtext2_disableClipping ~= -1) && ... ((ptb_drawformattedtext2_disableClipping > 0)); if isempty(ptb_drawformattedtext2_padthresh) % Threshold for skipping of text justification is 33% by default: ptb_drawformattedtext2_padthresh = 0.333; end padthresh = ptb_drawformattedtext2_padthresh; assert(Screen('Preference','TextRenderer') == 1, 'DrawFormattedText2 only works with the FTGL based text drawing plugin, but this plugin is not selected activated with Screen(''Preference'',''TextRenderer'',1), or did not load correctly. See help DrawTextPlugin for more information.'); % Optional per-word bounding boxes requested or should generate them % because cache requested? if (nargout >= 4) && (~IsOctave || isargout(4) || isargout(5)) dowordbounds = 1; else dowordbounds = 0; end if IsOctave % char() casts of unicode values > 255 map to zero, because Octave % uses UTF-8 encoding for unicode, instead of UTF-32 as Matlab. We % take care of this in the code, but necessary casts() also trigger % an out-of-range warning in Octave, which we can't selectively disable, % as it lacks a unique warning id (duh!). Therefore disable all warnings % on Octave and re-enable to previous setting whenever the we exit, and % therefore the canary variable reenablewarn goes out of scope: warningstate = warning('query'); warning('off'); reenablewarn = onCleanup(@() restorewarningstate(warningstate)); end %% process key-value input [opt,qCalledWithCache] = parseInputs(varargin,nargout); if isempty(opt) % nothing to do [nx, ny, textbounds, cache, wordbounds] = deal([]); return; elseif qCalledWithCache cache = opt.cache; if isfield(cache,'tex') [nx, ny] = deal(cache.nx,cache.ny); if ~opt.cacheOnly DoDrawTexture(opt.win,cache.tex.number,cache.bbox,cache.transform); end else if ~opt.cacheOnly [nx, ny, textbounds, wordbounds] = DoDraw(opt.win,... disableClip,... cache.px,... cache.py,... cache.bbox,... cache.subStrings,... cache.switches,... cache.fmts,... cache.fmtCombs,... cache.ssBaseLineOff,... cache.winRect,... cache.previous,... cache.righttoleft,... cache.transform,... cache.wordbounds); end end if isfield(cache,'tex') || opt.cacheOnly wordbounds = cache.wordbounds; if isempty(cache.transform) textbounds = cache.bbox; else % transform BBox and wordbounds to reflect transforms applied by % DoDrawSetup textbounds = transformBBox(cache.bbox,cache.transform); for b=1:size(wordbounds,1) wordbounds(b,:) = transformBBox2(wordbounds(b,:), cache.transform, cache.bbox); end end if ~isfield(cache,'tex') [nx,ny] = deal([]); end end % done return; end % normal draw, unpack input arguments [tstring,win,sx,sy,xalign,yalign,xlayout,baseColor,wrapat,transform,vSpacing,righttoleft,winRect,resetStyle,cacheOnly,cacheMode] = ... deal(opt.tstring,opt.win,opt.sx,opt.sy,opt.xalign,opt.yalign,opt.xlayout,opt.baseColor,opt.wrapat,opt.transform,opt.vSpacing,opt.righttoleft,opt.winRect,opt.resetStyle,opt.cacheOnly,opt.cacheMode); % Need different encoding for returnChar that matches class of input % tstring: returnChar = cast(10,class(tstring)); % Convert all conventional linefeeds into C-style newlines. % But if '\n' is already encoded as a char(10) as in Octave, then % there's no need for replacement. if char(10) ~= '\n' newlinepos = strfind(char(tstring), '\n'); while ~isempty(newlinepos) % Replace first occurrence of '\n' by ASCII or double code 10 aka 'repchar': tstring = [ tstring(1:min(newlinepos)-1) returnChar tstring(min(newlinepos)+2:end)]; % Search next occurrence of linefeed (if any) in new expanded string: newlinepos = strfind(char(tstring), '\n'); end end % string can contain HTML-like formatting commands. Parse them and turn % them into formatting indicators, then remove them from the string to draw [tstring,fmtCombs,fmts,switches,previous] = getFormatting(win,tstring,baseColor,resetStyle); % check we still have anything to render after formatting tags removed if isempty(tstring) % Empty text string -> Nothing to do, but assign dummy values: [nx, ny] = Screen('DrawText', win, ''); textbounds = [nx, ny, nx, ny]; wordbounds = textbounds; cache = []; return; end % Text wrapping requested? NB: formatting tags are removed above, so % wrapping is correct. Also NB that WrapString only replaces spaces by % linebreaks and thus does not alter the length of the string or where % words are placed in it. Our codes.style and codes.color vectors thus remain % correct. if wrapat > 0 % Call WrapString to create a broken up version of the input string % that is wrapped around column 'wrapat' tstring = WrapString(tstring, wrapat); end % now, split text into segments, either when there is a carriage return or % when the format changes % find format changes qSwitch = any(switches,1); % find carriage returns (can occur at same spot as format change) % make them their own substring so we can process format changes happening % at the carriage return properly. if dowordbounds % For per-word bounding boxes, each space as classified by isspace() % counts as a "carriage return" to force the code to split strings into % subStrings at word boundaries as well, not only carriage returns or % style changes: qCRet = tstring==returnChar | isspace(tstring); else % No per word bounding boxes needed: qCRet = tstring==returnChar; end qCRet = ismember(1:length(tstring),[find(qCRet) find(qCRet)+1]); % split strings qSplit = qSwitch|qCRet; % subStrings = accumarray(cumsum(qSplit).'+1,tstring(:),[],@(x) {x.'}); % own implementation to make sure it works on all platform (not sure how % well the accumarray trick works on Octave). Not any slower either strI = cumsum(qSplit).'+1; subStrings = cell(strI(end),1); for p=1:strI(end) subStrings{p} = tstring(strI==p); end % get which format to use for each substring, and what attributes are % changed (if any) fmtCombs = fmtCombs(qSplit); switches = switches(:,qSplit); % code when to perform linefeeds. qLineFeed= cellfun(@(x) ~isempty(x) && x(1)==returnChar,subStrings).'; % we have an empty up front if there is a format change or carriage return % first in the string if isempty(subStrings{1}) && ~qCRet(1) % remove it if it is a switch from the default format, but not if we % start with a carriage return subStrings(1) = []; qLineFeed(1) = []; else % we also need to know about the substring before the first split fmtCombs = [1 fmtCombs]; switches = [false(4,1) switches]; end % if trailing carriage return, this should lead to a trailing empty line, % add it here. if tstring(end)==returnChar subStrings{end+1} = ''; fmtCombs(end+1) = fmtCombs(end); switches(:,end+1) = switches(:,end); qLineFeed(end+1) = false; end % remove those linefeeds from the characters to draw qLineFeed = logical(qLineFeed); % i saw qLineFeed become a double on some older matlab versions... subStrings(qLineFeed) = cellfun(@(x) x(2:end),subStrings(qLineFeed),'uni',false); % NB: keep substrings that are now empty as they still signal linefeeds and % empty lines, and format changes can still occur for those empty substrings % get number of lines. numlines = length(strfind(char(tstring), char(10))) + 1; % vectors for metrics (like width and height) for each line, andor each % substring lWidth = zeros(1,numlines); lWidthOff = zeros(2,numlines); lBaseLineSkip = zeros(1,numlines); lBaseLineOff = zeros(2,numlines); lWidthOffLine = zeros(3,length(subStrings)); sWidth = zeros(1,length(subStrings)); px = zeros(1,length(subStrings)); py = zeros(1,length(subStrings)); ssBaseLineSkip = zeros(1,length(subStrings)); ssBaseLineOff = zeros(2,length(subStrings)); % process each substring, collect info per substring and line % get which substrings belong to each line substrIdxs = [0 cumsum(qLineFeed(1:end-1))]; if ~qLineFeed(1) substrIdxs = substrIdxs+1; end for p=1:numlines % get which substrings belong to this line qSubStr = substrIdxs==p; % to get line width and height, get textbounds of each string and add % them together for q=find(qSubStr) % do format change if needed if any(switches(:,q)) fmt = fmts(:,fmtCombs(q)); DoFormatChange(win,switches(:,q),fmt); end if isempty(subStrings{q}) [~,bbox,h] = Screen('TextBounds', win, 'x',0,0,1,righttoleft); xAdv = 0; else [~,bbox,h,xAdv] = Screen('TextBounds', win, subStrings{q},0,0,1,righttoleft); end % get amount cursor moves when drawing this substring sWidth(q) = xAdv; % get amount the substring's pixels extend below and above baseline ssBaseLineOff(:,q) = bbox([2 4]); % for proper linespacing, get text height (distance between % baselines as indicated in the font) ssBaseLineSkip(q) = h; % get info we need to construct precise bounding boxes, bbox is % pixel-precise, word length based on xAdv is too long. and first % pixels may appear after drawing cursor position, take that into % account if xAdv lWidthOffLine(1,q) = bbox(3)-bbox(1); lWidthOffLine(2,q) = bbox(1); lWidthOffLine(3,q) = xAdv-bbox(3); end end % get width of each line lWidth(p) = sum(sWidth(qSubStr)); % get largest baseline skip for each line lBaseLineSkip(p) = max(ssBaseLineSkip(qSubStr)); % get largest offset of ink from baseline for each line lBaseLineOff(:,p) = [min(ssBaseLineOff(1,qSubStr)) max(ssBaseLineOff(2,qSubStr))]; % get pixel offset for line % floor to deal with fractional positions, tiny bit of overlap with % pixel still leads to some paint being put there. is = find(lWidthOffLine(1,:),1); il = find(lWidthOffLine(1,:),1,'last'); if righttoleft % boxes get drawn in opposite order from input text, so: % find last with non-zero length if ~isempty(il) lWidthOff(1,p) = floor(lWidthOffLine(2,il)); end % find first with non-zero length if ~isempty(is) lWidthOff(2,p) = floor(lWidthOffLine(3,is)); end else % find first with non-zero length if ~isempty(is) lWidthOff(1,p) = floor(lWidthOffLine(2,is)); end % find last with non-zero length if ~isempty(il) lWidthOff(2,p) = floor(lWidthOffLine(3,il)); end end end % don't forget to set style back to what it should be ResetTextSetup(win,previous,false); % get pixel precise line widths lWidthPrecise = lWidth-sum(lWidthOff,1); % position the bounding box of the whole text mWidth = max(lWidthPrecise); if xlayout>4 justOff = ceil((xlayout-3)/3)*3; if justOff == 6 % justify to full width of winRect mWidth = winRect(3)-winRect(1); end end % from observing how Word layouts the text, about .22 of text height is % below the baseline (meaning .78 is above). It is important to mimic this, % a line with text that is much larger than the previous line will end up % too low, and a line with text that is much smaller than the previous line % will probably end up on top of the previous line's ink. totHeight = -lBaseLineOff(1,1) + sum(round((.22*lBaseLineSkip(1:end-1)+.78*lBaseLineSkip(2:end))*vSpacing)) + lBaseLineOff(2,end); bbox = [0 0 mWidth totHeight]; bbox = positionBbox(bbox,sx,sy,xalign,yalign); % now, figure out where to place individual lines and substrings into this % bbox for p=1:numlines % get which substrings belong to this line qSubStr = substrIdxs==p; idxs = find(qSubStr); % get center of line w.r.t. bbox left edge xlayoutLine = xlayout; if xlayout>3 % do some form of justification, setup % check if there are spaces so padding would be possible: % TODO % Required padding less than padthresh fraction of total width? If % not we skip justification, as it would lead to ridiculous looking % results: if lWidth(p) < mWidth * padthresh xlayoutLine = xlayout-justOff; % remove offset like this makes sure we have line aligned to left or to right of bbox, as requested end end switch xlayoutLine case 1 % align to left at sx lc = lWidth(p)/2; case {2,4} % center or justify line in bbox lc = (bbox(3)-bbox(1))/2; case 3 % align to right of window lc = bbox(3)-bbox(1) - lWidth(p)/2; end if righttoleft off = -cumsum( sWidth( qSubStr) ) + lWidth(p)/2; else off = cumsum([0 sWidth(idxs(1:end-1))]) - lWidth(p)/2; end if xlayoutLine==4 xSpace = 0; else xSpace = 0; end px(qSubStr) = lc+off-lWidthOff(1,p); % NB: we've been calculating with pixel/paint positions, go back to text cursor position to have the pixels end up where we want them if p>1 % add baseline skip for current line if not first line, that's the % carriage return. See note above about how Word does text layout. idx = find(qSubStr,1,'first'); py(idx:end) = py(idx) + round((.22*lBaseLineSkip(p-1)+.78*lBaseLineSkip(p))*vSpacing); else % we're drawing with yPositionIsBaseline==true, correct for that py(:) = -min(ssBaseLineOff(1,qSubStr)); end end % determine word (actually segment) bounds qNotEmpty = sWidth>0 & sum(abs(ssBaseLineOff),1); wordboundsbase = zeros(sum(qNotEmpty),4); wordboundsbase(:,1) = floor(px(qNotEmpty)+lWidthOffLine(2,qNotEmpty)); wordboundsbase(:,2) = floor(py(qNotEmpty)+ssBaseLineOff(1,qNotEmpty)); wordboundsbase(:,3) = ceil( px(qNotEmpty)+lWidthOffLine(2,qNotEmpty)+lWidthOffLine(1,qNotEmpty)); wordboundsbase(:,4) = ceil( py(qNotEmpty)+ssBaseLineOff(2,qNotEmpty)); % check if we're doing drawing via a texture. If so, we need a texture the % size of the bounding box, and we need to keep (px,py) w.r.t the bounding % box, not w.r.t. the screen qDrawToTexture = nargout >= 3 && cacheMode==1; % now we have positions and wordbounds w.r.t. the bbox, add bbox position % to place them in the right place on the screen if ~qDrawToTexture px = px+bbox(1); py = py+bbox(2); wordboundsbase(:,1) = wordboundsbase(:,1)+bbox(1); wordboundsbase(:,2) = wordboundsbase(:,2)+bbox(2); wordboundsbase(:,3) = wordboundsbase(:,3)+bbox(1); wordboundsbase(:,4) = wordboundsbase(:,4)+bbox(2); end %% done processing inputs, do text drawing % do draw to texture if wanted if qDrawToTexture texBbox = OffsetRect(bbox,-bbox(1),-bbox(2)); drawRect = transformBBox(texBbox,transform); extraTransOff = [0 0]; if ~isempty(transform) % Extra translate so that drawn text has top-left at (0,0) extraTransOff = -drawRect(1:2); end [tex.number,tex.rect] = Screen('OpenOffscreenWindow', win, [0 0 0 0], [0 0 RectWidth(drawRect) RectHeight(drawRect)]); ResetTextSetup(tex.number,previous,true); [nx, ny, ~, wordbounds] = DoDraw(tex.number,disableClip,px,py,texBbox,subStrings,switches,fmts,fmtCombs,ssBaseLineOff,winRect,previous,righttoleft,transform,wordboundsbase,extraTransOff); nx = nx+bbox(1); ny = ny+bbox(2); wordbounds(:,1) = wordbounds(:,1)+bbox(1); wordbounds(:,2) = wordbounds(:,2)+bbox(2); wordbounds(:,3) = wordbounds(:,3)+bbox(1); wordbounds(:,4) = wordbounds(:,4)+bbox(2); textbounds = OffsetRect(drawRect,bbox(1),bbox(2)); end if ~cacheOnly % draw to screen if qDrawToTexture DoDrawTexture(win,tex.number,textbounds,[]); else [nx, ny, textbounds, wordbounds] = DoDraw(win,disableClip,px,py,bbox,subStrings,switches,fmts,fmtCombs,ssBaseLineOff,winRect,previous,righttoleft,transform,wordboundsbase); end elseif cacheMode~=1 % don't do any drawing to screen, nor to texture [nx,ny] = deal([]); % the bbox in the cache is untranslated (we need to know the original % after all when drawing). Output transformed one here for user's info, % so they know what'll appear on screen eventually. textbounds = transformBBox(bbox,transform); wordbounds = wordboundsbase; for b=1:size(wordbounds,1) wordbounds(b,:) = transformBBox2(wordbounds(b,:), transform, bbox); end end if nargout>3 % make cache cache.opt = opt; cache.win = win; if cacheMode==1 cache.tex = tex; cache.bbox = textbounds; cache.transform = []; % no need to reapply transforms as they are already "hardcoded" into the texture. But user can add new ones cache.nx = nx; cache.ny = ny; cache.wordbounds = wordbounds; % store transformed wordbounds as they're "hardcoded" into the texture else cache.px = px; cache.py = py; cache.bbox = bbox; cache.subStrings = subStrings; cache.substrIdxs = substrIdxs; cache.switches = switches; cache.fmts = fmts; cache.fmtCombs = fmtCombs; cache.ssBaseLineOff = ssBaseLineOff; cache.winRect = winRect; cache.previous = previous; cache.righttoleft = righttoleft; cache.transform = transform; cache.wordbounds = wordboundsbase; end end end % Restore warning() settings to initial at onCleanup(): function restorewarningstate(warningstate) warning(warningstate); end function [previouswin, IsOpenGLRendering] = DoDrawSetup(win,transform,bbox,extraTransOff) if nargin<4 extraTransOff = [0 0]; end % Is the OpenGL userspace context for this 'windowPtr' active, as required? [previouswin, IsOpenGLRendering] = Screen('GetOpenGLDrawMode'); % OpenGL rendering for this window active? if IsOpenGLRendering % Yes. We need to disable OpenGL mode for that other window and % switch to our window: Screen('EndOpenGL', win); end if ~isempty(transform) [xc, yc] = RectCenterd(bbox); % Make a backup copy of the current transformation matrix for later % use/restoration of default state: Screen('glPushMatrix', win); % We need to undo the translation Screen('glTranslate', win, xc+extraTransOff(1), yc+extraTransOff(2)); % apply transforms % as OpenGL transform should be specified in reversed order but i % don't want to bother the interface with that, apply transform % back to front here. for p=length(transform)-1:-2:1 switch transform{p} case 'translate' Screen('glTranslate', win, transform{p+1}(1), transform{p+1}(2)); case 'flip' % argument is which axis, x (1), y (2), or both (3). Both % equals 180 degree rotation. if transform{p+1}(1)==1 Screen('glScale', win, -1, 1); elseif transform{p+1}(1)==2 Screen('glScale', win, 1, -1); elseif transform{p+1}(1)==3 Screen('glScale', win, -1, -1); end case 'scale' Screen('glScale', win, transform{p+1}(1), transform{p+1}(2)); case 'rotate' ang = transform{p+1}(1); % note that for the PTB screen, a positive rotation is % clockwise. Screen('glRotate', win, ang); end end % Translate origin to the geometric center of the text Screen('glTranslate', win, -xc, -yc); end end function DoDrawCleanup(win, previouswin, IsOpenGLRendering, transform) % undo transform if any if ~isempty(transform) Screen('glPopMatrix', win); end % If a different window than our target window was active, we'll switch % back to that window and its state: if previouswin > 0 if previouswin ~= win % Different window was active before our invocation: % Was that window in 3D mode, i.e., OpenGL rendering for that window was active? if IsOpenGLRendering % Yes. We need to switch that window back into 3D OpenGL mode: Screen('BeginOpenGL', previouswin); else % No. We just perform a dummy call that will switch back to that % window: Screen('GetWindowInfo', previouswin); end else % Our window was active beforehand. if IsOpenGLRendering % Was in 3D mode. We need to switch back to 3D: Screen('BeginOpenGL', previouswin); end end end end function DoDrawTexture(win,texNum,texDrawRect,transform) [previouswin, IsOpenGLRendering] = DoDrawSetup(win,transform,texDrawRect); Screen('DrawTexture',win,texNum,[],texDrawRect); DoDrawCleanup(win, previouswin, IsOpenGLRendering, transform); end function [nx, ny, bbox, wordbounds] = DoDraw(win,disableClip,sx,sy,bbox,subStrings,switches,fmts,fmtCombs,ssBaseLineOff,winRect,previous,righttoleft,transform,wordboundsbase,extraTransOff) [nx,ny] = deal(nan); wordbounds = wordboundsbase; if nargin<16 extraTransOff = [0 0]; end [previouswin, IsOpenGLRendering] = DoDrawSetup(win, transform, bbox, extraTransOff); if ~isempty(transform) % transform BBox and wordbounds to reflect transforms applied by % DoDrawSetup for b=1:size(wordbounds,1) wordbounds(b,:) = transformBBox2(wordbounds(b,:), transform, bbox); end bbox = transformBBox(bbox,transform); end % Draw the substrings for p=1:length(subStrings) curstring = subStrings{p}; yp = sy(p); xp = sx(p); % do format change if needed if any(switches(:,p)) fmt = fmts(:,fmtCombs(p)); DoFormatChange(win,switches(:,p),fmt); end % Perform crude clipping against upper and lower window borders for this text snippet. % If it is clearly outside the window and would get clipped away by the renderer anyway, % we can safe ourselves the trouble of processing it: if ~isempty(curstring) && (disableClip || ((yp + ssBaseLineOff(2,p) >= winRect(2)) && (yp + ssBaseLineOff(1,p) <= winRect(4)))) % Inside crude clipping area. Need to draw. clipOrEmpty = false; else % Skip this text line draw call, as it is empty or would be clipped % away anyway. clipOrEmpty = true; end % Any string to draw? if ~clipOrEmpty % The cursor is positioned (nx,ny output) to allow to continue to % print text directly after the drawn text (if you set % yPositionIsBaseline==true at least). Basically behaves like % printf or fprintf formatting. [nx,ny] = Screen('DrawText', win, curstring, xp, yp,[],[],1, righttoleft); % for debug, draw bounding box and baseline % [~,sbbox,~,xAdv] = Screen('TextBounds', win, curstring, xp, yp, 1, righttoleft); % Screen('FrameRect',win,[0 255 0 128],sbbox); % Screen('DrawDots',win,[xp+xAdv ny],1,[0 0 255 128]); % xp+xAdv equal nx, but not necessarily left edge of next bbox % Screen('DrawLine',win,[0 255 255],xp,yp,nx,ny); end end % Our work is done. clean up % reset text style etc ResetTextSetup(win,previous,false); DoDrawCleanup(win, previouswin, IsOpenGLRendering, transform); end %% helpers function [tstring,fmtCombs,fmts,switches,previous] = getFormatting(win,tstring,startColor,resetStyle) % This function parses tags out of the text and turns them into formatting % textstyles, colors, font and text sizes to use when drawing. % allowable codes: % - To toggle italicization % - To toggle bolding % - To toggle underlining % - To switch to a new color % - To switch to a new font % - To switch to a new font size % get string type, store original as octave can't deal with string values outside uint8 range tstringOri = tstring; tstring = char(tstring); tstring(tstring>127) = 0; % get colorrange of window, to interpret colors cr = Screen('ColorRange',win); % get current active text options previous.style = Screen('TextStyle', win); previous.size = Screen('TextSize' , win); previous.font = Screen('TextFont' , win); % baseColor is given as input as its convenient for user to be able to set % it and consistent with other text drawing functions. previous.color = startColor; % keep copy of numeric representation (if its hex, its converted to numeric below when checking base.color) % get starting text options base = previous; if resetStyle % start with a clean-slate style (no bold, italic, underlined, etc) % when interpreting formatting commands base.style = 0; end % convert color to hex if isnumeric(base.color) if cr==1.0 % convert to comma separated floating point base.color = sprintf('%f,',base.color); base.color(end) = []; % remove trailing comma else % convert to hex base.color = sprintf('%0*X',[repmat(2,size(base.color));base.color]); end else % if user provided color input, store in format we can use in PTB later if any(previous.color=='.')||any(previous.color==',') % user provided floating point input previous.color = sscanf(previous.color,'%f,').'; else % user provided hex input previous.color = hex2dec(reshape(previous.color,2,[]).').'; base.color = upper(base.color); % ensure hex color is uppercase so below logic works correctly end end % prepare outputs % these outputs have same length as string to draw and for each character % indicate its style and color codes.style = repmat(base.style,size(tstring)); tables.color = {base.color}; tables.font = {base.font}; codes.color = ones(size(tstring)); % 1 indicates startColor, all is in startColor unless user provides color tags telling us otherwise codes.font = codes.color; % 1 indicates default font, all is in default font unless user provides color tags telling us otherwise codes.size = repmat(previous.size,size(tstring)); %% first process tags that don't have further specifiers (, , ) % find tag locations. (?','start','end','tokens'); if ~isempty(tagis) % get full text for each tags and indices to where it is in the input % string tagi = [tagis; tagie].'; tagt = cat(1,tagt{:}); % fill up output, indicating the style code applicable to each % character if ~isempty(tagt) currStyle = codes.style(1); for p=1:length(tagt) % the below code snippet is a comment, describing what the line % below does % switch formatCodes{p} % case 'i' % fBit = log2(2)+1; % case 'b' % fBit = log2(1)+1; % case 'u' % fBit = log2(4)+1; % end fBit = floor((double(tagt{p})-'b')/7)+1; currStyle = bitset(currStyle,fBit,~bitget(currStyle,fBit)); codes.style(tagi(p,2):end) = currStyle; end end % now mark active formatting commands to be stripped from text toStrip = bsxfun(@plus,tagi(:,1),0:2); % tags are always three characters long toStrip = toStrip(:).'; else toStrip = []; end %% now process tag that have further specifiers (, , ) % find tag locations. also match empty tags. even if only empty tags, we % still want to remove them. Ill formed tags with equals sign but no % argument, or tags with tags inside, are not matched. (?]+?)>|(?','start','end','tokens'); if ~isempty(tagis) % get full text for each tag and indices to where it is in the input % string tagi = [tagis; tagie].'; % use a simple stack/state machine as we need to maintain a history. % empty tags means go back to previous color/size/font % (crappy stacks, end of array is top of stack) colorStack = 1; % index in tables.color fontStack = 1; % index in tables.font sizeStack = codes.size(1); for p=1:size(tagi,1) % check if tag has argument if ~isscalar(tagt{p}) switch tagt{p}{1} case 'color' color = tagt{p}{2}; % check color is valid % detect FPN: if comma or decimal point qComma = color==','; if any(color=='.') || any(qComma) assert(all(isstrprop(color,'digit')|color=='.'|qComma),'DrawFormattedText2: color tag argument must be specified as comma-separated floating point values, or hexadecimal values') else assert(any(length(color)==[1 2 6 8]),'DrawFormattedText2: if color tag argument is a hexadecimal value, it should have length 1, 2, 6, or 8') assert(all(isstrprop(color,'xdigit')),'DrawFormattedText2: color tag argument must be specified as hexadecimal values, or comma-separated floating point values') end % find new color or add to table iColor = find(strcmpi(tables.color,color),1); if isempty(iColor) tables.color{end+1} = upper(color); iColor = length(tables.color); end % add to stack front colorStack(end+1) = iColor; %#ok % mark all next text as having this color codes.color(tagi(p,2):end) = iColor; case 'font' font = tagt{p}{2}; % no checks on whether it is valid % find new color or add to table iFont = find(strcmpi(tables.font,font),1); if isempty(iFont) tables.font{end+1} = font; iFont = length(tables.font); end % add to stack front fontStack(end+1) = iFont; %#ok % mark all next text as having this color codes.font(tagi(p,2):end) = iFont; case 'size' fsize = str2double(tagt{p}{2}); assert(~isnan(fsize),'DrawFormattedText2: size tag argument must be a number') % add to stack front sizeStack(end+1) = fsize; %#ok % mark all next text as having this color codes.size(tagi(p,2):end) = fsize; end else switch tagt{p}{1} case 'color' % if not already reached end of history, revert to % previous color for rest of text if ~isscalar(colorStack) % pop color of stack colorStack(end) = []; % mark all next text as having this color codes.color(tagi(p,2):end) = colorStack(end); end case 'font' % if not already reached end of history, revert to % previous color for rest of text if ~isscalar(fontStack) % pop font of stack fontStack(end) = []; % mark all next text as having this font codes.font(tagi(p,2):end) = fontStack(end); end case 'size' % if not already reached end of history, revert to % previous color for rest of text if ~isscalar(sizeStack) % pop size of stack sizeStack(end) = []; % mark all next text as having this size codes.size(tagi(p,2):end) = sizeStack(end); end end end end % now mark active formatting commands to be stripped from text (NB: % despite growing array, this is faster than something preallocated) for p=1:size(tagi,1) toStrip = [toStrip tagi(p,1):tagi(p,2)]; %#ok end end % now strip active formatting commands from text % add escape slashes from any escaped tags. also when double slashed, % we should remove one toStrip = [toStrip regexp(tstring,'(?i)/<(i|b|u|color|font|size)','start')]; tstringOri (toStrip) = []; codes.style(toStrip) = []; codes.color(toStrip) = []; codes.font (toStrip) = []; codes.size (toStrip) = []; % replace tstring with tstringOri again in case input was outside char range, so Octave can handle this all just fine.. tstring = tstringOri; if isempty(tstring) % string was only formatting commands, nothing to draw, ignore [fmtCombs,fmts,switches,previous] = deal([]); return; end % process colors, hex->dec for p=1:length(tables.color) qComma = tables.color{p}==','; if any(tables.color{p}=='.') || any(qComma) % at this point, all we know is that the string contains digits, % decimal points and commas (checked above). tables.color{p} = sscanf(tables.color{p},'%f,'); assert(any(length(tables.color{p})==[1 3 4]),'DrawFormattedText2: if color tag argument is a comma-separated floating point value, it should have length 1, 3, or 4') if cr==255 % scale tables.color{p} = round(tables.color{p}.*255); end else % above we made sure all colors are uppercase and valid hex % then, convert letter to their numerical value % -48 for numbers (ascii<=64) % -55 for letters (ascii>64) tables.color{p} = tables.color{p}-48-(tables.color{p}>64)*7; % then, sum in pairs, while multiplying first of each pair by its base, 16 tables.color{p} = sum([tables.color{p}(1:2:end)*16;tables.color{p}(2:2:end)]); if cr==1.0 % scale tables.color{p} = tables.color{p}./255; end end end % consolidate codes into one, indicating unique combinations. Also produce % four boolean vectors indicating what changed upon a style change. % last, output a table that for each unique combination indicates what the % style, font, color and size are c = [codes.style; codes.color; codes.font; codes.size]; % where do changes occur? switches = logical(diff([[previous.style; 1; 1; previous.size] c],[],2)); % make sure color is always applied, may have been changed under our feet % if drawn later, or may have been provided as baseColor by user switches(2,1) = true; % get unique formats and where each of these formats is to be applied % the below is equivalent to: % [format,~,fmtCombs] = unique(c.','rows'); % format = format.'; % fmtCombs = fmtCombs.'; % but do required functionality myself to be way faster: if IsOctave() [~,i] = sortrows(c.',1:4); else i=sortrowsc(c.',1:4); end groupsSortA = [true any(c(:,i(1:end-1)) ~= c(:,i(2:end)),1)]; format = c(:,i(groupsSortA)); fmtCombs = cumsum(groupsSortA); fmtCombs(i) = fmtCombs; % build table with info about each unique format combination fmts = num2cell(format); % two columns are indices into table, do indexing fmts(2,:) = tables.color(format(2,:)); fmts(3,:) = tables.font (format(3,:)); % last, store which need to be changed back when drawing finished previous.changed = logical(diff([[previous.style; 1; 1; previous.size] c(:,end)],[],2)); end function DoFormatChange(win,switches,fmt) % rows in switches / columns in format: % 1: style, 2: color, 3: font, 4: size % font and style: if we change font, always set style with the same % command. Always works and sometimes needed with some exotic fonts % (see Screen('TextFont?') ) if switches(3) Screen('TextFont', win,fmt{3},fmt{1}); elseif switches(1) Screen('TextStyle',win,fmt{1}); end % color, set through this command. drawing commands below do not % set color if switches(2) Screen('TextColor',win,fmt{2}); end % size if switches(4) Screen('TextSize',win,fmt{4}); end end function ResetTextSetup(win,previous,qDoAll) if qDoAll || previous.changed(3) Screen('TextFont',win,previous.font,previous.style); elseif previous.changed(1) Screen('TextStyle',win,previous.style); end if qDoAll || previous.changed(2) Screen('TextColor',win,previous.color); % setting the baseColor input, not color before function entered. Consistent with other text drawing functions end if qDoAll || previous.changed(4) Screen('TextSize',win,previous.size); end end function bbox = positionBbox(bbox,sx,sy,xalign,yalign) bWidth = bbox(3)-bbox(1); bHeight= bbox(4)-bbox(2); switch xalign case 1 xoff = 0; case 2 xoff = -bWidth/2; case 3 xoff = -bWidth; end switch yalign case 1 yoff = 0; case 2 yoff = -bHeight/2; case 3 yoff = -bHeight; end bbox = OffsetRect(bbox,round(sx+xoff),round(sy+yoff)); end function bbox = transformBBox(bbox,transform) if ~isempty(transform) [xc, yc] = RectCenterd(bbox); bbox = OffsetRect(bbox,-xc,-yc); % apply transforms for p=1:2:length(transform) switch transform{p} case 'translate' bbox = OffsetRect(bbox, transform{p+1}(1), transform{p+1}(2)); case 'flip' % argument is which axis, x (1), y (2), or both (3). Both % equals 180 degree rotation. All are no-ops here as % bounding box does not change case 'scale' bbox = ScaleRect(bbox, transform{p+1}(1), transform{p+1}(2)); case 'rotate' ang = transform{p+1}(1); % note that for the PTB screen, a positive rotation is % clockwise. bbox = [cosd(ang) -sind(ang); sind(ang) cosd(ang)] * bbox([1 3 1 3; 2 4 4 2]); bbox = [min(bbox,[],2).' max(bbox,[],2).']; end end % We need to undo the translations... bbox = OffsetRect(bbox,xc,yc); end end function bbox = transformBBox2(bbox,transform,refbox) if ~isempty(transform) M = eye(3,3); [xc, yc] = RectCenterd(refbox); M = M * [[1, 0, xc]; [0, 1, yc]; [0, 0, 1]]; % apply transforms for p=1:2:length(transform) switch transform{p} case 'translate' M = M * [[1, 0, transform{p+1}(1)]; [0, 1, transform{p+1}(2)]; [0, 0, 1]]; case 'flip' % argument is flip around which axis, y (1), x (2), or both (3). if transform{p+1}(1) == 1 M = M * [[-1, 0, 0]; [0, 1, 0]; [0, 0, 1]]; end if transform{p+1}(1) == 2 M = M * [[1, 0, 0]; [0, -1, 0]; [0, 0, 1]]; end if transform{p+1}(1) == 3 M = M * [[-1, 0, 0]; [0, -1, 0]; [0, 0, 1]]; end case 'scale' M = M * [[transform{p+1}(1), 0, 0]; [0, transform{p+1}(2), 0]; [0, 0, 1]]; case 'rotate' ang = transform{p+1}(1); % note that for the PTB screen, a positive rotation is % clockwise. M = M * [[cosd(ang), -sind(ang), 0] ; [sind(ang), cosd(ang) 0] ; [0, 0, 1]]; end end % We need to undo the translations... M = M * [[1, 0, -xc]; [0, 1, -yc]; [0, 0, 1]]; % Apply combined transforms: v1 = M * [bbox(1); bbox(2); 1]; v2 = M * [bbox(3); bbox(2); 1]; v3 = M * [bbox(3); bbox(4); 1]; v4 = M * [bbox(1); bbox(4); 1]; % Project back to 2D plane: v1 = v1 / v1(3); v2 = v2 / v2(3); v3 = v3 / v3(3); v4 = v4 / v4(3); % Make axis-aligned: bbox = [min([v1(1),v2(1),v3(1),v4(1)]), min([v1(2),v2(2),v3(2),v4(2)]), max([v1(1),v2(1),v3(1),v4(1)]), max([v1(2),v2(2),v3(2),v4(2)])]; end end function [opt,qCalledWithCache] = parseInputs(varargs,nOutArg) if isempty(varargs) || isempty(varargs{1}) % Empty text string -> Nothing to do. opt = []; qCalledWithCache = false; return; elseif isstruct(varargs{1}) % called with cache qCalledWithCache = true; qTextureCache = isfield(varargs{1},'tex'); opt = struct(... 'cache',varargs{1},... 'win',[],... 'sx' ,[],... 'sy' ,[],... 'xalign' ,[],... 'yalign' ,[],... 'transform',[],... 'cacheOnly',false,... 'winRect',[]... ); else opt = struct(... 'tstring',varargs{1},... 'win',[],... 'sx' ,[],... 'sy' ,[],... 'xalign' ,[],... 'yalign' ,[],... 'xlayout' ,1,... 'baseColor',[],... 'wrapat',0,... 'transform',[],... 'vSpacing',1,... 'righttoleft',0,... 'winRect',[],... 'resetStyle',1,... 'cacheOnly',false,... 'cacheMode',1 ... ); qCalledWithCache = false; end % parse inputs assert(mod(length(varargs),2)==1,'function should be called with key-value pairs for all inputs except the first.') for p=2:2:length(varargs) assert(isfield(opt,varargs{p}),'option %s not understood',varargs{p}) opt.(varargs{p}) = varargs{p+1}; end % further check and process inputs if qCalledWithCache if isempty(opt.win) opt.win = opt.cache.win; end if ~qTextureCache ResetTextSetup(opt.win,opt.cache.previous,true); end % check which if any of the below are set ignorep = [isempty(opt.sx) isempty(opt.sy) isempty(opt.xalign) isempty(opt.yalign)]; else if isempty(opt.win) error('DrawFormattedText2: Windowhandle missing!'); end % layout of individual lines within the bounding box % default is aligned to left of box if ~ischar(opt.xlayout) opt.xlayout = 1; else switch opt.xlayout case 'left' opt.xlayout = 1; case 'center' opt.xlayout = 2; case 'right' opt.xlayout = 3; % not implemented: 4, 6, 7, 9 case 'ljustifylongest' % justify to width of longest sentence. if line is too short (see ptb_drawformattedtext2_padthresh), left align opt.xlayout = 4; % -3==1 case 'rjustifylongest' % justify to width of longest sentence. if line is too short (see ptb_drawformattedtext2_padthresh), right align opt.xlayout = 6; % -3==3 case 'ljustifyfullwidth' % justify to width of winRect. if line is too short (see ptb_drawformattedtext2_padthresh), left align opt.xlayout = 7; % -6==1 case 'rjustifyfullwidth' % justify to width of winRect. if line is too short (see ptb_drawformattedtext2_padthresh), right align opt.xlayout = 9; % -6==3 otherwise % ignore anything else user may have provided opt.xlayout = 1; end end % Keep current text color if none provided: if isempty(opt.baseColor) opt.baseColor = Screen('TextColor', opt.win); else opt.baseColor = opt.baseColor(:).'; % ensure row vector end % No text wrapping by default: if isinf(opt.wrapat) opt.wrapat = 0; end % option to only generate cache but not draw if ~isempty(opt.cacheOnly) opt.cacheOnly = logical(opt.cacheOnly); end % check cache mode if ischar(opt.cacheMode) switch opt.cacheMode case 'texture' opt.cacheMode = 1; case 'fullCache' opt.cacheMode = 2; otherwise error('cache mode "%s" not understood, possible values: "texture" and "fullCache"',opt.cacheMode); end else assert(ismember(opt.cacheMode,[1 2]),'cacheMode must be 1 or 2') end if opt.cacheOnly && nOutArg < 4 error('cacheOnly requested but fourth output (cache) is not requested') end end % No mirroring/rotating/etc of text by default: if ~isempty(opt.transform) checktransform(opt.transform); end % Default rectangle for centering/formatting text is the client rectangle % of the window, but usercode can specify arbitrary override it: if isempty(opt.winRect) opt.winRect = Screen('Rect', opt.win); qWinRectSpecified = false; else assert(isnumeric(opt.winRect) && numel(opt.winRect)==4,'Provided winRect is not valid') qWinRectSpecified = true; end % position text box on screen [opt.sx,opt.sy,opt.xalign,opt.yalign] = parseTextBoxPositioning(opt.sx,opt.sy,opt.xalign,opt.yalign,opt.winRect); % do any last extra processing if drawing from cache. if qCalledWithCache if ~all(ignorep) % all true: no repositioning or offsetting % move bbox as requested bbox = opt.cache.bbox; if any(~ignorep(3:4)) % first, fill out missing arguments. for f={'sx','sy','xalign','yalign'} if isempty(opt.(f{1})) opt.(f{1}) = opt.cache.opt.(f{1}); end end % reposition bbox: bbox = positionBbox([0 0 bbox(3)-bbox(1) bbox(4)-bbox(2)],opt.sx,opt.sy,opt.xalign,opt.yalign); % bbox(1:2) now contain new top-left for text. Make that into % offsets to reposition text off = bbox(1:2)-opt.cache.bbox(1:2); else % first, fill out missing arguments. for f={'sx','sy'} if isempty(opt.(f{1})) opt.(f{1}) = 0; end end % only sx and sy provided, do offsetting assert(isnumeric(opt.sx)&&isnumeric(opt.sy),'When drawing from cache and providing horizontal and vertical offsets with the ''sx'' and ''sy'' inputs, these offsets must be numeric') off = [opt.sx opt.sy]; bbox = OffsetRect(opt.cache.bbox,off(1),off(2)); end % common logic: put bounding box and text in new place if ~qTextureCache opt.cache.px = opt.cache.px + off(1); opt.cache.py = opt.cache.py + off(2); end % apply offsets to word bounding boxes too off = bbox-opt.cache.bbox; opt.cache.wordbounds(:,1) = opt.cache.wordbounds(:,1)+off(1); opt.cache.wordbounds(:,2) = opt.cache.wordbounds(:,2)+off(2); opt.cache.wordbounds(:,3) = opt.cache.wordbounds(:,3)+off(1); opt.cache.wordbounds(:,4) = opt.cache.wordbounds(:,4)+off(2); opt.cache.bbox = bbox; end % overwrite winRect in cache if set by user if qWinRectSpecified opt.cache.winRect = opt.winRect; end % append to transforms, if any provided by user if ~isempty(opt.transform) opt.cache.transform = [opt.cache.transform opt.transform]; end end end function [sx,sy,xalign,yalign] = parseTextBoxPositioning(sx,sy,xalign,yalign,winRect) xpos = 0; % default: use provided sx (w.r.t. winRect) % Default x start position is left of window: if isempty(sx) sx = 0; else % have text specifying a position at the edge of windowrect? if ischar(sx) if strcmpi(sx, 'left') xpos = 1; elseif strcmpi(sx, 'center') xpos = 2; elseif strcmpi(sx, 'right') xpos = 3; else % Ignore any other crap user may have provided, align to left. xpos = 1; end elseif ~isnumeric(sx) % Ignore any other crap user may have provided. sx = 0; end end ypos = 0; % default: use provided sy (w.r.t. winRect) % Default y start position is top of window: if isempty(sy) sy = 0; else % have text specifying a position at the edge of windowrect? if ischar(sy) if strcmpi(sy, 'top') ypos = 1; elseif strcmpi(sy, 'center') ypos = 2; elseif strcmpi(sy, 'bottom') ypos = 3; else % Ignore any other crap user may have provided, align to left. ypos = 1; end elseif ~isnumeric(sy) % Ignore any other crap user may have provided. sy = 0; end end % now process xpos and ypos switch xpos case 0 % provided sx is in winRect sx = sx+winRect(1); case 1 sx = winRect(1); case 2 sx = (winRect(1)+winRect(3))/2; case 3 sx = winRect(3); end switch ypos case 0 % provided sy is in winRect sy = sy+winRect(2); case 1 sy = winRect(2); case 2 sy = (winRect(2)+winRect(4))/2; case 3 sy = winRect(4); end %%% now we have a position, figure out how to position box with respect to %%% that position % Default x layout is align box to left of specified position if isempty(xalign) || ~ischar(xalign) xalign = 1; else if strcmpi(xalign, 'left') xalign = 1; elseif strcmpi(xalign, 'center') xalign = 2; elseif strcmpi(xalign, 'right') xalign = 3; else % ignore anything else user may have provided xalign = 1; end end % Default y layout is align box below of specified position if isempty(yalign) || ~ischar(yalign) yalign = 1; else if strcmpi(yalign, 'top') yalign = 1; elseif strcmpi(yalign, 'center') yalign = 2; elseif strcmpi(yalign, 'bottom') yalign = 3; else % ignore anything else user may have provided yalign = 1; end end end function checktransform(transform) if isempty(transform) return; end assert(mod(numel(transform),2)==0,'transform input must be key-value') assert(iscellstr(transform(1:2:end)),'transform: all keys should be a character array') assert(all(cellfun(@isnumeric,transform(2:2:end))),'transform: all values should be a numeric') end