How to render text in a CALayer using NSLayoutManager? - cocoa

I am using the trio of NSLayoutManager, NSTextStorage & NSTextContainer to render text in a layer-backed NSView.
I am under the impression that the view will be more performant if I can override wantsUpdateLayer to return true and thus be using layers more fully. Is this true?
However, all examples I have seen of using NSLayoutManager say that you need to place the following call within drawRect:
layoutManager.drawGlyphsForGlyphRange(glyphRange, atPoint: NSMakePoint(0, 0))
How would you perform the equivalent call within updateLayer or some other more layer-oriented place instead?

I created a custom CALayer and overrode the drawInContext method as below:
override func drawInContext(ctx: CGContext!) {
NSGraphicsContext.saveGraphicsState()
var nsgc = NSGraphicsContext(CGContext: ctx, flipped: false)
NSGraphicsContext.setCurrentContext(nsgc)
NSColor.whiteColor().setFill()
NSRectFill(NSMakeRect(0, 0, 84, 24))
lm.drawGlyphsForGlyphRange(glyphRange, atPoint: NSMakePoint(0, 0)) // location of textContainer
NSGraphicsContext.restoreGraphicsState()
}
This gets the job done, but
I'm not sure if there are any performance implications to saving and restoring the graphics context as above.
I am unsure how the same would be achieved in the NSView's updateLayer method as that does not have a context to use.

Related

Stop NSView drawRect clearing before drawing? (lockFocus no longer working on macOS 10.14)

I have an animation using two views where I would call lockFocus and get at the graphics context of the second NSView with [[NSGraphicsContext currentContext] graphicsPort], and draw. That worked fine until macOS 10.14 (Mojave).
Found a reference here:
https://developer.apple.com/videos/play/wwdc2018/209/
At 22:40 they talk about the "legacy" backing store that has changed. The above lockFocus and context pattern was big on the screen, saying that that won't work any more. And that is true. lockFocus() still works, and even gets you the correct NSView, but any drawing via the context does not work any more.
Of course the proper way to draw in a view is via Nsview's drawRect. So I rearranged everything to do just that. That works, but, drawRect has already automatically cleared the "dirty" area for you, prior to calling your drawRect. If you used setNeedsDisplayInRect: it will even clear only those areas for you. But, it will also clear areas made up of more than one dirty rectangle. And, it clears rectangular areas, while I draw roundish objects, so I end up with too much cleared away (black area):
Is there a way to prevent drawRect to clear the background?
If not I will have switch to using the NSView's layer instead, and use updateLayer, or something.
Update: I am playing around with using the layers of NSView, returning TRUE for wantsLayer and wantsUpdateLayer, and implementing:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
That is only called when I do:
- (BOOL) wantsLayer { return YES; }
- (BOOL) wantsUpdateLayer { return NO; }
and use setNeedsDisplayInRect: or setNeedsDisplay:
Which indeed makes drawRect no longer called, but the same automatic background erase has already taken place by the time my drawLayer: is called. So I have not made any progress there. It really is the effect of setNeedsDisplayInRect, and not my drawing. Just calling setNeedsDisplayInRect causes these erases.
If you set:
- (BOOL) wantsLayer { return YES; }
- (BOOL) wantsUpdateLayer { return YES; }
the only thing that is called is:
- (void) updateLayer
which does not provide me with a context to draw.
I had some hope for:
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawNever;
The NSView doc says:
Leave the layer's contents alone. Never mark the layer as needing
display, or draw the view's contents to the layer. This is how
developer created layers (layer-hosting views) are treated.
and it does exactly that. It doesn't notify you, or call delegates.
Is there a way to have complete control over the content of an NSView/layer?
UPDATE 2019-JAN-27:
NSView has a method makeBackingLayer that is not there for nothing, I guess. Implemented that, and it seems to work, basically, but no output shows on screen. Hence the followup question: nsview-makebackinglayer-with-calayer-subclass-displays-no-output
Using NSView lockFocus and unlockFocus, or trying to access the window's graphics contents directly not working in macOS 10.14 anymore.
You can create an NSBitmapImageRep object and update drawing in this context.
Sample code:
- (void)drawRect:(NSRect)rect {
if (self.cachedDrawingRep) {
[self.cachedDrawingRep drawInRect:self.bounds];
}
}
- (void)drawOutsideDrawRect {
NSBitmapImageRep *cmap = self.cachedDrawingRep;
if (!cmap) {
cmap = [self bitmapImageRepForCachingDisplayInRect:self.bounds];
self.cachedDrawingRep = cmap;
}
NSGraphicsContext *ctx = [NSGraphicsContext graphicsContextWithBitmapImageRep:cmap];
NSAssert(ctx, nil);
[NSGraphicsContext setCurrentContext:ctx];
// Draw code here
self.needsDisplay = YES;
}
You can try overriding method isOpaque and return YES from there. This will tell OS that we draw all pixels ourselves and it do not need to draw the views/window at the back of our view.
- (BOOL)isOpaque {
return YES;
}

Why is an empty drawRect interfering with a simple animation?

I'm trying to put together my own take on Cocoa's NSPathControl.
In a bid to figure out the best way to handle the expand-contract animation that you get when you mouse over a component in the control, I put together a very simple sample app. Initially things were going well - mouse into or out of the view containing the path components and you'd get a simple animation using the following code:
// This code belongs to PathView (not PathComponentView)
override func mouseEntered(theEvent: NSEvent) {
NSAnimationContext.runAnimationGroup({ (ctx) -> Void in
self.animateToProportions([CGFloat](arrayLiteral: 0.05, 0.45, 0.45, 0.05))
}, completionHandler: { () -> Void in
//
})
}
// Animating subviews
func animateToProportions(proportions: [CGFloat]) {
let height = self.bounds.height
let width = self.bounds.width
var xOffset: CGFloat = 0
for (i, proportion) in enumerate(proportions) {
let subview = self.subviews[i] as! NSView
let newFrame = CGRect(x: xOffset, y: 0, width: width * proportion, height: height)
subview.animator().frame = newFrame
xOffset = subview.frame.maxX
}
}
As the next step in the control's development, instead of using NSView instances as my path components I started using my own NSView subclass:
class PathComponentView: NSView {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
}
}
Although essentially identical to NSView, when I use this class in my animation, the animation no longer does what it's supposed to - the frames I set for the subviews are pretty much ignored, and the whole effect is ugly. If I comment out the drawRect: method of my subclass things return to normal. Can anyone explain what's going wrong? Why is the presence of drawRect: interfering with the animation?
I've put a demo project on my GitHub page.
You've broken some rules here, and when you break the rules, behavior becomes undefined.
In a layer backed view, you must not modify the layer directly. See the documentation for wantsLayer (which is how you specify that it's layer-backed):
In a layer-backed view, any drawing done by the view is cached to the underlying layer object. This cached content can then be manipulated in ways that are more performant than redrawing the view contents explicitly. AppKit automatically creates the underlying layer object (using the makeBackingLayer method) and handles the caching of the view’s content. If the wantsUpdateLayer method returns NO, you should not interact with the underlying layer object directly. Instead, use the methods of this class to make any changes to the view and its layer. If wantsUpdateLayer returns YES, it is acceptable (and appropriate) to modify the layer in the view’s updateLayer method.
You make this call:
subview.layer?.backgroundColor = color.CGColor
That's "interact[ing] with the underlying layer object directly." You're not allowed to do that. When there's no drawRect, AppKit skips trying to redraw the view and just uses Core Animation to animate what's there, which gives you what you expect (you're just getting lucky there; it's not promised this will work).
But when it sees you actually implemented drawRect (it knows you did, but it doesn't know what's inside), then it has to call it to perform custom drawing. It's your responsibility to make sure that drawRect fills in every pixel of the rectangle. You can't rely on the backing layer to do that for you (that's just a cache). You draw nothing, so what you're seeing is the default background gray intermixed with cached color. There's no promise that all of the pixels will be updated correctly once we've gone down this road.
If you want to manipulate the layer, you want a "layer-hosting view" not a "layer-backed view." You can do that by replacing AppKit's cache layer with your own custom layer.
let subview = showMeTheProblem ? PathComponentView() : NSView()
addSubview(subview)
subview.layer = CALayer() // Use our layer, not AppKit's caching layer.
subview.layer?.backgroundColor = color.CGColor
While it probably doesn't matter in this case, keep in mind that a layer-backed view and a layer-hosting view have different performance characteristics. In a layer-backed view, AppKit is caching your drawRect results into its caching layer, and then applying animations on that fairly automatically (this allows existing AppKit code that was written before CALayer to get some nice opt-in performance improvements without changing much). In a layer-hosting view, the system is more like iOS. You don't get any magical caching. There's just a layer, and you can use Core Animation on it.

How do I implement dragging of an NSImage inside of an NSView?

I've never dealt much with NSViews so I am at a loss on this. I have a custom class that contains an NSImage ivar. I also have a subclass of NSView that contains an array of instances of my custom class. The NSImage from the custom class gets drawn at the center of the NSView upon creation. I need to be able to drag that NSImage around the NSView freely.
I have implemented the following methods to get started:
-(void)mouseDown:(NSEvent *)theEvent
{
[self beginDraggingSessionWithItems: /* I don't know what to put here */
event:theEvent
source:self];
}
I have tried placing the NSImage object from my custom class in an array as the item to drag but an exception is raised. I also tried creating an NSDraggingItem to place in there but it could not write the object to the pasteboard and raised an exception.
-(void)draggingSession:(NSDraggingSession *)session willBeginAtPoint:(NSPoint)screenPoint
{
NSLog(#"began");
}
-(void)draggingSession:(NSDraggingSession *)session movedToPoint:(NSPoint)screenPoint
{
NSLog(#"moved");
}
-(void)draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation
{
NSLog(#"ended");
}
These methods get called properly as I click and drag my mouse around my subclass of NSView however they are useless until I can get the object to move.
I believe the answer may be to create an NSDraggingItem for each image and use that to move them freely around the view however I could not get it to work with even one object and do not know how to properly implement this idea. Any help is appreciated.
Went through a lot of headache on this one but finally figured it out. Instead of using beginDraggingSessionWithItems:event:source I used dragImage:at:offset:event:pasteboard:source:slideBack:. I keep track of the coordinates of each image in the view and can tell whether a specific image was clicked on or not by utilizing the following method I wrote in mouseDown:
-(BOOL)clickedAtPoint:(NSPoint)point InsideRect:(NSRect)rect
{
if ((point.x < (rect.origin.x + rect.size.width) &&
point.y < (rect.origin.y + rect.size.height)) &&
point.x > rect.origin.x && point.y > rect.origin.y) {
return YES;
}
return NO;
}

Can I use CALayer to speed up view rendering?

I am making a custom NSView object that has some content that changes often, and some that changes much more infrequently. As it would turn out, the parts that change less often take the most time to draw. What I would like to do is render these two parts in different layers, so that I can update one or the other separately, thus sparing my user a sluggish user interface.
How might I go about doing this? I have not found many good tutorials on this sort of thing, and none that talk about rendering NSBezierPaths on a CALayer. Ideas anyone?
Your hunch is right, this is actually an excellent way to optimise drawing. I've done it myself where I had some large static backgrounds that I wanted to avoid redrawing when elements moved on top.
All you need to do is add CALayer objects for each of the content items in your view. To draw the layers, you should set your view as the delegate for each layer and then implement the drawLayer:inContext: method.
In that method you just draw the content of each layer:
- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx
{
if(layer == yourBackgroundLayer)
{
//draw your background content in the context
//you can either use Quartz drawing directly in the CGContextRef,
//or if you want to use the Cocoa drawing objects you can do this:
NSGraphicsContext* drawingContext = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:YES];
NSGraphicsContext* previousContext = [NSGraphicsContext currentContext];
[NSGraphicsContext setCurrentContext:drawingContext];
[NSGraphicsContext saveGraphicsState];
//draw some stuff with NSBezierPath etc
[NSGraphicsContext restoreGraphicsState];
[NSGraphicsContext setCurrentContext:previousContext];
}
else if (layer == someOtherLayer)
{
//draw other layer
}
//etc etc
}
When you want to update the content of one of the layers, just call [yourLayer setNeedsDisplay]. This will then call the delegate method above to provide the updated content of the layer.
Note that by default, when you change the layer content, Core Animation provides a nice fade transition for the new content. However, if you're handling the drawing yourself you probably don't want this, so in order to prevent the default fade in animation when the layer content changes, you also have to implement the actionForLayer:forKey: delegate method and prevent the animation by returning a null action:
- (id<CAAction>)actionForLayer:(CALayer*)layer forKey:(NSString*)key
{
if(layer == someLayer)
{
//we don't want to animate new content in and out
if([key isEqualToString:#"contents"])
{
return (id<CAAction>)[NSNull null];
}
}
//the default action for everything else
return nil;
}

How to draw to an NSView graphics context from inside a CVDisplayLink callback?

I have a simple game that renders 2D graphics to a frame buffer (not using any OpenGL). I was going to use a CVDisplayLink to get a clean framerate, however most examples on the web deal with OpenGL or QuickTime.
So far I have a sub class of NSView:
#interface GameView : NSView {
#private
CVDisplayLinkRef displayLink;
}
- (CVReturn)getFrameForTime:(const CVTimeStamp*)outputTime;
#end
And I set up the CVDisplayLink callback:
CVDisplayLinkSetOutputCallback(displayLink, MyDisplayLinkCallback, self);
And I have the callback function:
CVReturn MyDisplayLinkCallback (CVDisplayLinkRef displayLink,
const CVTimeStamp *inNow,
const CVTimeStamp *inOutputTime,
CVOptionFlags flagsIn,
CVOptionFlags *flagsOut,
void *displayLinkContext)
{
CVReturn error = [(GameView*)displayLinkContext getFrameForTime:inOutputTime];
return error;
}
The part where I'm stuck is what to do in getFrameForTime: to draw to the graphics context in the GameView. My first guess was to do the drawing the same way you would in drawRect:
- (CVReturn)getFrameForTime:(const CVTimeStamp*)outputTime
{
CGContextRef ctxCurrent = [[NSGraphicsContext currentContext] graphicsPort];
//.. Drawing code follows
}
But ctxCurrent is nil which I think I understand - normally there is some setup that happens before the drawRect: that makes your view the current context. I think this is the part I'm missing. How do I get the context for my view?
Or am I going about this in all the wrong ways?
You could leave your drawing code in ‑drawRect: and then in your MyDisplayLinkCallback() set an ivar to the current time and call ‑display on your view. This will force your view to immediately redraw itself.
In your ‑drawRect: method, just use the value of the time ivar to do whatever drawing is necessary to update the view appropriately for the current animation frame.
You should create a separate -draw: method, and call that from your MyDisplayLinkCallback() as well as from -drawRect:.
I found Rob Keniger's response to my liking and tried it out; sadly, if you call -display from your display link callback your app may hang in a deadlock when you terminate the app, leaving you to force-quit the app (for me it happened more often than not).

Resources