| // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "webkit/glue/plugins/pepper_graphics_2d.h" |
| |
| #include <iterator> |
| |
| #include "base/logging.h" |
| #include "base/message_loop.h" |
| #include "base/task.h" |
| #include "gfx/blit.h" |
| #include "gfx/point.h" |
| #include "gfx/rect.h" |
| #include "skia/ext/platform_canvas.h" |
| #include "ppapi/c/pp_errors.h" |
| #include "ppapi/c/pp_module.h" |
| #include "ppapi/c/pp_rect.h" |
| #include "ppapi/c/pp_resource.h" |
| #include "ppapi/c/ppb_graphics_2d.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "webkit/glue/plugins/pepper_common.h" |
| #include "webkit/glue/plugins/pepper_image_data.h" |
| #include "webkit/glue/plugins/pepper_plugin_instance.h" |
| #include "webkit/glue/plugins/pepper_plugin_module.h" |
| |
| #if defined(OS_MACOSX) |
| #include "base/mac_util.h" |
| #include "base/mac/scoped_cftyperef.h" |
| #endif |
| |
| namespace pepper { |
| |
| namespace { |
| |
| // Converts a rect inside an image of the given dimensions. The rect may be |
| // NULL to indicate it should be the entire image. If the rect is outside of |
| // the image, this will do nothing and return false. |
| bool ValidateAndConvertRect(const PP_Rect* rect, |
| int image_width, int image_height, |
| gfx::Rect* dest) { |
| if (!rect) { |
| // Use the entire image area. |
| *dest = gfx::Rect(0, 0, image_width, image_height); |
| } else { |
| // Validate the passed-in area. |
| if (rect->point.x < 0 || rect->point.y < 0 || |
| rect->size.width <= 0 || rect->size.height <= 0) |
| return false; |
| |
| // Check the max bounds, being careful of overflow. |
| if (static_cast<int64>(rect->point.x) + |
| static_cast<int64>(rect->size.width) > |
| static_cast<int64>(image_width)) |
| return false; |
| if (static_cast<int64>(rect->point.y) + |
| static_cast<int64>(rect->size.height) > |
| static_cast<int64>(image_height)) |
| return false; |
| |
| *dest = gfx::Rect(rect->point.x, rect->point.y, |
| rect->size.width, rect->size.height); |
| } |
| return true; |
| } |
| |
| // Converts BGRA <-> RGBA. |
| void ConvertBetweenBGRAandRGBA(const uint32_t* input, |
| int pixel_length, |
| uint32_t* output) { |
| for (int i = 0; i < pixel_length; i++) { |
| const unsigned char* pixel_in = |
| reinterpret_cast<const unsigned char*>(&input[i]); |
| unsigned char* pixel_out = reinterpret_cast<unsigned char*>(&output[i]); |
| pixel_out[0] = pixel_in[2]; |
| pixel_out[1] = pixel_in[1]; |
| pixel_out[2] = pixel_in[0]; |
| pixel_out[3] = pixel_in[3]; |
| } |
| } |
| |
| // Converts ImageData from PP_IMAGEDATAFORMAT_BGRA_PREMUL to |
| // PP_IMAGEDATAFORMAT_RGBA_PREMUL, or reverse. |
| void ConvertImageData(ImageData* src_image, const SkIRect& src_rect, |
| ImageData* dest_image, const SkRect& dest_rect) { |
| DCHECK(src_image->format() != dest_image->format()); |
| DCHECK(ImageData::IsImageDataFormatSupported(src_image->format())); |
| DCHECK(ImageData::IsImageDataFormatSupported(dest_image->format())); |
| |
| const SkBitmap* src_bitmap = src_image->GetMappedBitmap(); |
| const SkBitmap* dest_bitmap = dest_image->GetMappedBitmap(); |
| if (src_rect.width() == src_image->width() && |
| dest_rect.width() == dest_image->width()) { |
| // Fast path if the full line needs to be converted. |
| ConvertBetweenBGRAandRGBA( |
| src_bitmap->getAddr32(static_cast<int>(src_rect.fLeft), |
| static_cast<int>(src_rect.fTop)), |
| src_rect.width() * src_rect.height(), |
| dest_bitmap->getAddr32(static_cast<int>(dest_rect.fLeft), |
| static_cast<int>(dest_rect.fTop))); |
| } else { |
| // Slow path where we convert line by line. |
| for (int y = 0; y < src_rect.height(); y++) { |
| ConvertBetweenBGRAandRGBA( |
| src_bitmap->getAddr32(static_cast<int>(src_rect.fLeft), |
| static_cast<int>(src_rect.fTop + y)), |
| src_rect.width(), |
| dest_bitmap->getAddr32(static_cast<int>(dest_rect.fLeft), |
| static_cast<int>(dest_rect.fTop + y))); |
| } |
| } |
| } |
| |
| PP_Resource Create(PP_Module module_id, |
| const PP_Size* size, |
| PP_Bool is_always_opaque) { |
| PluginModule* module = ResourceTracker::Get()->GetModule(module_id); |
| if (!module) |
| return 0; |
| |
| scoped_refptr<Graphics2D> context(new Graphics2D(module)); |
| if (!context->Init(size->width, size->height, PPBoolToBool(is_always_opaque))) |
| return 0; |
| return context->GetReference(); |
| } |
| |
| PP_Bool IsGraphics2D(PP_Resource resource) { |
| return BoolToPPBool(!!Resource::GetAs<Graphics2D>(resource)); |
| } |
| |
| PP_Bool Describe(PP_Resource graphics_2d, |
| PP_Size* size, |
| PP_Bool* is_always_opaque) { |
| scoped_refptr<Graphics2D> context( |
| Resource::GetAs<Graphics2D>(graphics_2d)); |
| if (!context) |
| return PP_FALSE; |
| return context->Describe(size, is_always_opaque); |
| } |
| |
| void PaintImageData(PP_Resource graphics_2d, |
| PP_Resource image_data, |
| const PP_Point* top_left, |
| const PP_Rect* src_rect) { |
| scoped_refptr<Graphics2D> context( |
| Resource::GetAs<Graphics2D>(graphics_2d)); |
| if (context) |
| context->PaintImageData(image_data, top_left, src_rect); |
| } |
| |
| void Scroll(PP_Resource graphics_2d, |
| const PP_Rect* clip_rect, |
| const PP_Point* amount) { |
| scoped_refptr<Graphics2D> context( |
| Resource::GetAs<Graphics2D>(graphics_2d)); |
| if (context) |
| context->Scroll(clip_rect, amount); |
| } |
| |
| void ReplaceContents(PP_Resource graphics_2d, PP_Resource image_data) { |
| scoped_refptr<Graphics2D> context( |
| Resource::GetAs<Graphics2D>(graphics_2d)); |
| if (context) |
| context->ReplaceContents(image_data); |
| } |
| |
| int32_t Flush(PP_Resource graphics_2d, |
| PP_CompletionCallback callback) { |
| scoped_refptr<Graphics2D> context( |
| Resource::GetAs<Graphics2D>(graphics_2d)); |
| if (!context) |
| return PP_ERROR_BADRESOURCE; |
| return context->Flush(callback); |
| } |
| |
| const PPB_Graphics2D ppb_graphics_2d = { |
| &Create, |
| &IsGraphics2D, |
| &Describe, |
| &PaintImageData, |
| &Scroll, |
| &ReplaceContents, |
| &Flush |
| }; |
| |
| } // namespace |
| |
| struct Graphics2D::QueuedOperation { |
| enum Type { |
| PAINT, |
| SCROLL, |
| REPLACE |
| }; |
| |
| QueuedOperation(Type t) |
| : type(t), |
| paint_x(0), |
| paint_y(0), |
| scroll_dx(0), |
| scroll_dy(0) { |
| } |
| |
| Type type; |
| |
| // Valid when type == PAINT. |
| scoped_refptr<ImageData> paint_image; |
| int paint_x, paint_y; |
| gfx::Rect paint_src_rect; |
| |
| // Valid when type == SCROLL. |
| gfx::Rect scroll_clip_rect; |
| int scroll_dx, scroll_dy; |
| |
| // Valid when type == REPLACE. |
| scoped_refptr<ImageData> replace_image; |
| }; |
| |
| Graphics2D::Graphics2D(PluginModule* module) |
| : Resource(module), |
| bound_instance_(NULL), |
| flushed_any_data_(false), |
| offscreen_flush_pending_(false), |
| is_always_opaque_(false) { |
| } |
| |
| Graphics2D::~Graphics2D() { |
| } |
| |
| // static |
| const PPB_Graphics2D* Graphics2D::GetInterface() { |
| return &ppb_graphics_2d; |
| } |
| |
| bool Graphics2D::Init(int width, int height, bool is_always_opaque) { |
| // The underlying ImageData will validate the dimensions. |
| image_data_ = new ImageData(module()); |
| if (!image_data_->Init(ImageData::GetNativeImageDataFormat(), width, height, |
| true) || !image_data_->Map()) { |
| image_data_ = NULL; |
| return false; |
| } |
| is_always_opaque_ = is_always_opaque; |
| return true; |
| } |
| |
| PP_Bool Graphics2D::Describe(PP_Size* size, PP_Bool* is_always_opaque) { |
| size->width = image_data_->width(); |
| size->height = image_data_->height(); |
| *is_always_opaque = PP_FALSE; // TODO(brettw) implement this. |
| return PP_TRUE; |
| } |
| |
| void Graphics2D::PaintImageData(PP_Resource image_data, |
| const PP_Point* top_left, |
| const PP_Rect* src_rect) { |
| if (!top_left) |
| return; |
| |
| scoped_refptr<ImageData> image_resource( |
| Resource::GetAs<ImageData>(image_data)); |
| if (!image_resource) |
| return; |
| |
| QueuedOperation operation(QueuedOperation::PAINT); |
| operation.paint_image = image_resource; |
| if (!ValidateAndConvertRect(src_rect, image_resource->width(), |
| image_resource->height(), |
| &operation.paint_src_rect)) |
| return; |
| |
| // Validate the bitmap position using the previously-validated rect, there |
| // should be no painted area outside of the image. |
| int64 x64 = static_cast<int64>(top_left->x); |
| int64 y64 = static_cast<int64>(top_left->y); |
| if (x64 + static_cast<int64>(operation.paint_src_rect.x()) < 0 || |
| x64 + static_cast<int64>(operation.paint_src_rect.right()) > |
| image_data_->width()) |
| return; |
| if (y64 + static_cast<int64>(operation.paint_src_rect.y()) < 0 || |
| y64 + static_cast<int64>(operation.paint_src_rect.bottom()) > |
| image_data_->height()) |
| return; |
| operation.paint_x = top_left->x; |
| operation.paint_y = top_left->y; |
| |
| queued_operations_.push_back(operation); |
| } |
| |
| void Graphics2D::Scroll(const PP_Rect* clip_rect, const PP_Point* amount) { |
| QueuedOperation operation(QueuedOperation::SCROLL); |
| if (!ValidateAndConvertRect(clip_rect, |
| image_data_->width(), |
| image_data_->height(), |
| &operation.scroll_clip_rect)) |
| return; |
| |
| // If we're being asked to scroll by more than the clip rect size, just |
| // ignore this scroll command and say it worked. |
| int32 dx = amount->x; |
| int32 dy = amount->y; |
| if (dx <= -image_data_->width() || dx >= image_data_->width() || |
| dy <= -image_data_->height() || dy >= image_data_->height()) |
| return; |
| |
| operation.scroll_dx = dx; |
| operation.scroll_dy = dy; |
| |
| queued_operations_.push_back(operation); |
| } |
| |
| void Graphics2D::ReplaceContents(PP_Resource image_data) { |
| scoped_refptr<ImageData> image_resource( |
| Resource::GetAs<ImageData>(image_data)); |
| if (!image_resource) |
| return; |
| if (!ImageData::IsImageDataFormatSupported(image_resource->format())) |
| return; |
| |
| if (image_resource->width() != image_data_->width() || |
| image_resource->height() != image_data_->height()) |
| return; |
| |
| QueuedOperation operation(QueuedOperation::REPLACE); |
| operation.replace_image = image_resource; |
| queued_operations_.push_back(operation); |
| } |
| |
| int32_t Graphics2D::Flush(const PP_CompletionCallback& callback) { |
| // Don't allow more than one pending flush at a time. |
| if (HasPendingFlush()) |
| return PP_ERROR_INPROGRESS; |
| |
| // TODO(brettw) check that the current thread is not the main one and |
| // implement blocking flushes in this case. |
| if (!callback.func) |
| return PP_ERROR_BADARGUMENT; |
| |
| bool nothing_visible = true; |
| for (size_t i = 0; i < queued_operations_.size(); i++) { |
| QueuedOperation& operation = queued_operations_[i]; |
| gfx::Rect op_rect; |
| switch (operation.type) { |
| case QueuedOperation::PAINT: |
| ExecutePaintImageData(operation.paint_image, |
| operation.paint_x, operation.paint_y, |
| operation.paint_src_rect, |
| &op_rect); |
| break; |
| case QueuedOperation::SCROLL: |
| ExecuteScroll(operation.scroll_clip_rect, |
| operation.scroll_dx, operation.scroll_dy, |
| &op_rect); |
| break; |
| case QueuedOperation::REPLACE: |
| ExecuteReplaceContents(operation.replace_image, &op_rect); |
| break; |
| } |
| |
| // We need the rect to be in terms of the current clip rect of the plugin |
| // since that's what will actually be painted. If we issue an invalidate |
| // for a clipped-out region, WebKit will do nothing and we won't get any |
| // ViewInitiatedPaint/ViewFlushedPaint calls, leaving our callback stranded. |
| gfx::Rect visible_changed_rect; |
| if (bound_instance_ && !op_rect.IsEmpty()) |
| visible_changed_rect = bound_instance_->clip().Intersect(op_rect); |
| |
| if (bound_instance_ && !visible_changed_rect.IsEmpty()) { |
| if (operation.type == QueuedOperation::SCROLL) { |
| bound_instance_->ScrollRect(operation.scroll_dx, operation.scroll_dy, |
| visible_changed_rect); |
| } else { |
| bound_instance_->InvalidateRect(visible_changed_rect); |
| } |
| nothing_visible = false; |
| } |
| } |
| queued_operations_.clear(); |
| flushed_any_data_ = true; |
| |
| if (nothing_visible) { |
| // There's nothing visible to invalidate so just schedule the callback to |
| // execute in the next round of the message loop. |
| ScheduleOffscreenCallback(FlushCallbackData(callback)); |
| } else { |
| unpainted_flush_callback_.Set(callback); |
| } |
| return PP_ERROR_WOULDBLOCK; |
| } |
| |
| bool Graphics2D::ReadImageData(PP_Resource image, |
| const PP_Point* top_left) { |
| // Get and validate the image object to paint into. |
| scoped_refptr<ImageData> image_resource(Resource::GetAs<ImageData>(image)); |
| if (!image_resource) |
| return false; |
| if (!ImageData::IsImageDataFormatSupported(image_resource->format())) |
| return false; // Must be in the right format. |
| |
| // Validate the bitmap position. |
| int x = top_left->x; |
| if (x < 0 || |
| static_cast<int64>(x) + static_cast<int64>(image_resource->width()) > |
| image_data_->width()) |
| return false; |
| int y = top_left->y; |
| if (y < 0 || |
| static_cast<int64>(y) + static_cast<int64>(image_resource->height()) > |
| image_data_->height()) |
| return false; |
| |
| ImageDataAutoMapper auto_mapper(image_resource); |
| if (!auto_mapper.is_valid()) |
| return false; |
| |
| SkIRect src_irect = { x, y, |
| x + image_resource->width(), |
| y + image_resource->height() }; |
| SkRect dest_rect = { SkIntToScalar(0), |
| SkIntToScalar(0), |
| SkIntToScalar(image_resource->width()), |
| SkIntToScalar(image_resource->height()) }; |
| |
| ImageDataAutoMapper auto_mapper2(image_data_); |
| if (image_resource->format() != image_data_->format()) { |
| // Convert the image data if the format does not match. |
| ConvertImageData(image_data_, src_irect, image_resource.get(), dest_rect); |
| } else { |
| skia::PlatformCanvas* dest_canvas = image_resource->mapped_canvas(); |
| |
| // We want to replace the contents of the bitmap rather than blend. |
| SkPaint paint; |
| paint.setXfermodeMode(SkXfermode::kSrc_Mode); |
| dest_canvas->drawBitmapRect(*image_data_->GetMappedBitmap(), |
| &src_irect, dest_rect, &paint); |
| } |
| return true; |
| } |
| |
| bool Graphics2D::BindToInstance(PluginInstance* new_instance) { |
| if (bound_instance_ == new_instance) |
| return true; // Rebinding the same device, nothing to do. |
| if (bound_instance_ && new_instance) |
| return false; // Can't change a bound device. |
| |
| if (!new_instance) { |
| // When the device is detached, we'll not get any more paint callbacks so |
| // we need to clear the list, but we still want to issue any pending |
| // callbacks to the plugin. |
| if (!unpainted_flush_callback_.is_null()) { |
| ScheduleOffscreenCallback(unpainted_flush_callback_); |
| unpainted_flush_callback_.Clear(); |
| } |
| if (!painted_flush_callback_.is_null()) { |
| ScheduleOffscreenCallback(painted_flush_callback_); |
| painted_flush_callback_.Clear(); |
| } |
| } else if (flushed_any_data_) { |
| // Only schedule a paint if this backing store has had any data flushed to |
| // it. This is an optimization. A "normal" plugin will first allocated a |
| // backing store, bind it, and then execute their normal painting and |
| // update loop. If binding a device always invalidated, it would mean we |
| // would get one paint for the bind, and one for the first time the plugin |
| // actually painted something. By not bothering to schedule an invalidate |
| // when an empty device is initially bound, we can save an extra paint for |
| // many plugins during the critical page initialization phase. |
| new_instance->InvalidateRect(gfx::Rect()); |
| } |
| |
| bound_instance_ = new_instance; |
| return true; |
| } |
| |
| void Graphics2D::Paint(WebKit::WebCanvas* canvas, |
| const gfx::Rect& plugin_rect, |
| const gfx::Rect& paint_rect) { |
| ImageDataAutoMapper auto_mapper(image_data_); |
| const SkBitmap& backing_bitmap = *image_data_->GetMappedBitmap(); |
| |
| #if defined(OS_MACOSX) |
| SkAutoLockPixels lock(backing_bitmap); |
| |
| base::mac::ScopedCFTypeRef<CGDataProviderRef> data_provider( |
| CGDataProviderCreateWithData( |
| NULL, backing_bitmap.getAddr32(0, 0), |
| backing_bitmap.rowBytes() * backing_bitmap.height(), NULL)); |
| base::mac::ScopedCFTypeRef<CGImageRef> image( |
| CGImageCreate( |
| backing_bitmap.width(), backing_bitmap.height(), |
| 8, 32, backing_bitmap.rowBytes(), |
| mac_util::GetSystemColorSpace(), |
| kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host, |
| data_provider, NULL, false, kCGRenderingIntentDefault)); |
| |
| // Flip the transform |
| CGContextSaveGState(canvas); |
| float window_height = static_cast<float>(CGBitmapContextGetHeight(canvas)); |
| CGContextTranslateCTM(canvas, 0, window_height); |
| CGContextScaleCTM(canvas, 1.0, -1.0); |
| |
| CGRect bounds; |
| bounds.origin.x = plugin_rect.origin().x(); |
| bounds.origin.y = window_height - plugin_rect.origin().y() - |
| backing_bitmap.height(); |
| bounds.size.width = backing_bitmap.width(); |
| bounds.size.height = backing_bitmap.height(); |
| |
| // TODO(brettw) bug 56673: do a direct memcpy instead of going through CG |
| // if the is_always_opaque_ flag is set. |
| |
| CGContextDrawImage(canvas, bounds, image); |
| CGContextRestoreGState(canvas); |
| #else |
| SkPaint paint; |
| if (is_always_opaque_) { |
| // When we know the device is opaque, we can disable blending for slightly |
| // more optimized painting. |
| paint.setXfermodeMode(SkXfermode::kSrc_Mode); |
| } |
| |
| gfx::Point origin(plugin_rect.origin().x(), plugin_rect.origin().y()); |
| canvas->drawBitmap(backing_bitmap, |
| SkIntToScalar(plugin_rect.origin().x()), |
| SkIntToScalar(plugin_rect.origin().y()), |
| &paint); |
| #endif |
| } |
| |
| void Graphics2D::ViewInitiatedPaint() { |
| // Move any "unpainted" callback to the painted state. See |
| // |unpainted_flush_callback_| in the header for more. |
| if (!unpainted_flush_callback_.is_null()) { |
| DCHECK(painted_flush_callback_.is_null()); |
| std::swap(painted_flush_callback_, unpainted_flush_callback_); |
| } |
| } |
| |
| void Graphics2D::ViewFlushedPaint() { |
| // Notify any "painted" callback. See |unpainted_flush_callback_| in the |
| // header for more. |
| if (!painted_flush_callback_.is_null()) { |
| // We must clear this variable before issuing the callback. It will be |
| // common for the plugin to issue another invalidate in response to a flush |
| // callback, and we don't want to think that a callback is already pending. |
| FlushCallbackData callback; |
| std::swap(callback, painted_flush_callback_); |
| callback.Execute(PP_OK); |
| } |
| } |
| |
| void Graphics2D::ExecutePaintImageData(ImageData* image, |
| int x, int y, |
| const gfx::Rect& src_rect, |
| gfx::Rect* invalidated_rect) { |
| // Ensure the source image is mapped to read from it. |
| ImageDataAutoMapper auto_mapper(image); |
| if (!auto_mapper.is_valid()) |
| return; |
| |
| // Portion within the source image to cut out. |
| SkIRect src_irect = { src_rect.x(), src_rect.y(), |
| src_rect.right(), src_rect.bottom() }; |
| |
| // Location within the backing store to copy to. |
| *invalidated_rect = src_rect; |
| invalidated_rect->Offset(x, y); |
| SkRect dest_rect = { SkIntToScalar(invalidated_rect->x()), |
| SkIntToScalar(invalidated_rect->y()), |
| SkIntToScalar(invalidated_rect->right()), |
| SkIntToScalar(invalidated_rect->bottom()) }; |
| |
| if (image->format() != image_data_->format()) { |
| // Convert the image data if the format does not match. |
| ConvertImageData(image, src_irect, image_data_, dest_rect); |
| } else { |
| // We're guaranteed to have a mapped canvas since we mapped it in Init(). |
| skia::PlatformCanvas* backing_canvas = image_data_->mapped_canvas(); |
| |
| // We want to replace the contents of the bitmap rather than blend. |
| SkPaint paint; |
| paint.setXfermodeMode(SkXfermode::kSrc_Mode); |
| backing_canvas->drawBitmapRect(*image->GetMappedBitmap(), |
| &src_irect, dest_rect, &paint); |
| } |
| } |
| |
| void Graphics2D::ExecuteScroll(const gfx::Rect& clip, int dx, int dy, |
| gfx::Rect* invalidated_rect) { |
| gfx::ScrollCanvas(image_data_->mapped_canvas(), |
| clip, gfx::Point(dx, dy)); |
| *invalidated_rect = clip; |
| } |
| |
| void Graphics2D::ExecuteReplaceContents(ImageData* image, |
| gfx::Rect* invalidated_rect) { |
| if (image->format() != image_data_->format()) { |
| DCHECK(image->width() == image_data_->width() && |
| image->height() == image_data_->height()); |
| // Convert the image data if the format does not match. |
| SkIRect src_irect = { 0, 0, image->width(), image->height() }; |
| SkRect dest_rect = { SkIntToScalar(0), |
| SkIntToScalar(0), |
| SkIntToScalar(image_data_->width()), |
| SkIntToScalar(image_data_->height()) }; |
| ConvertImageData(image, src_irect, image_data_, dest_rect); |
| } else { |
| image_data_->Swap(image); |
| } |
| *invalidated_rect = gfx::Rect(0, 0, |
| image_data_->width(), image_data_->height()); |
| } |
| |
| void Graphics2D::ScheduleOffscreenCallback(const FlushCallbackData& callback) { |
| DCHECK(!HasPendingFlush()); |
| offscreen_flush_pending_ = true; |
| MessageLoop::current()->PostTask( |
| FROM_HERE, |
| NewRunnableMethod(this, |
| &Graphics2D::ExecuteOffscreenCallback, |
| callback)); |
| } |
| |
| void Graphics2D::ExecuteOffscreenCallback(FlushCallbackData data) { |
| DCHECK(offscreen_flush_pending_); |
| |
| // We must clear this flag before issuing the callback. It will be |
| // common for the plugin to issue another invalidate in response to a flush |
| // callback, and we don't want to think that a callback is already pending. |
| offscreen_flush_pending_ = false; |
| data.Execute(PP_OK); |
| } |
| |
| bool Graphics2D::HasPendingFlush() const { |
| return !unpainted_flush_callback_.is_null() || |
| !painted_flush_callback_.is_null() || |
| offscreen_flush_pending_; |
| } |
| |
| } // namespace pepper |